Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
67.16% |
90 / 134 |
|
43.75% |
7 / 16 |
CRAP | |
0.00% |
0 / 1 |
ObjectCacheFactory | |
67.16% |
90 / 134 |
|
43.75% |
7 / 16 |
167.02 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultKeyspace | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
newFromId | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
8.81 | |||
getInstance | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromParams | |
72.00% |
18 / 25 |
|
0.00% |
0 / 1 |
8.08 | |||
prepareSqlBagOStuffFromParams | |
39.13% |
9 / 23 |
|
0.00% |
0 / 1 |
27.27 | |||
prepareMemcachedBagOStuffFromParams | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
prepareMultiWriteBagOStuffFromParams | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
prepareRESTBagOStuffFromParams | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLocalServerInstance | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
clear | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLocalServerCacheClass | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
12.41 | |||
getAnythingId | |
94.74% |
18 / 19 |
|
0.00% |
0 / 1 |
8.01 | |||
makeLocalServerCache | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
isDatabaseId | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
getLocalClusterInstance | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\Config\ServiceOptions; |
22 | use MediaWiki\Deferred\DeferredUpdates; |
23 | use MediaWiki\Http\Telemetry; |
24 | use MediaWiki\Logger\Spi; |
25 | use MediaWiki\MainConfigNames; |
26 | use MediaWiki\MediaWikiServices; |
27 | use Wikimedia\ObjectCache\APCUBagOStuff; |
28 | use Wikimedia\ObjectCache\BagOStuff; |
29 | use Wikimedia\ObjectCache\EmptyBagOStuff; |
30 | use Wikimedia\ObjectCache\HashBagOStuff; |
31 | use Wikimedia\ObjectCache\MemcachedBagOStuff; |
32 | use Wikimedia\ObjectCache\MultiWriteBagOStuff; |
33 | use Wikimedia\ObjectCache\RESTBagOStuff; |
34 | use Wikimedia\Stats\StatsFactory; |
35 | |
36 | /** |
37 | * Factory for cache objects as configured in the ObjectCaches setting. |
38 | * |
39 | * The word "cache" has two main dictionary meanings, and both |
40 | * are used in this factory class. They are: |
41 | * |
42 | * - a) Cache (the computer science definition). |
43 | * A place to store copies or computations on existing data for |
44 | * higher access speeds. |
45 | * - b) Storage. |
46 | * A place to store lightweight data that is not canonically |
47 | * stored anywhere else (e.g. a "hoard" of objects). |
48 | * |
49 | * Primary entry points: |
50 | * |
51 | * - ObjectCacheFactory::getLocalServerInstance( $fallbackType ) |
52 | * Purpose: Memory cache for very hot keys. |
53 | * Stored only on the individual web server (typically APC or APCu for web requests, |
54 | * and EmptyBagOStuff in CLI mode). |
55 | * Not replicated to the other servers. |
56 | * |
57 | * - ObjectCacheFactory::getLocalClusterInstance() |
58 | * Purpose: Memory storage for per-cluster coordination and tracking. |
59 | * A typical use case would be a rate limit counter or cache regeneration mutex. |
60 | * Stored centrally within the local data-center. Not replicated to other DCs. |
61 | * Configured by $wgMainCacheType. |
62 | * |
63 | * - ObjectCacheFactory::getInstance( $cacheType ) |
64 | * Purpose: Special cases (like tiered memory/disk caches). |
65 | * Get a specific cache type by key in $wgObjectCaches. |
66 | * |
67 | * All the above BagOStuff cache instances have their makeKey() |
68 | * method scoped to the *current* wiki ID. Use makeGlobalKey() to avoid this scoping |
69 | * when using keys that need to be shared amongst wikis. |
70 | * |
71 | * @ingroup Cache |
72 | * @since 1.42 |
73 | */ |
74 | class ObjectCacheFactory { |
75 | /** |
76 | * @internal For use by ServiceWiring.php |
77 | */ |
78 | public const CONSTRUCTOR_OPTIONS = [ |
79 | MainConfigNames::SQLiteDataDir, |
80 | MainConfigNames::UpdateRowsPerQuery, |
81 | MainConfigNames::MemCachedServers, |
82 | MainConfigNames::MemCachedPersistent, |
83 | MainConfigNames::MemCachedTimeout, |
84 | MainConfigNames::CachePrefix, |
85 | MainConfigNames::ObjectCaches, |
86 | MainConfigNames::MainCacheType, |
87 | MainConfigNames::MessageCacheType, |
88 | MainConfigNames::ParserCacheType, |
89 | ]; |
90 | |
91 | private ServiceOptions $options; |
92 | private StatsFactory $stats; |
93 | private Spi $logger; |
94 | /** @var BagOStuff[] */ |
95 | private $instances = []; |
96 | private string $domainId; |
97 | /** @var callable */ |
98 | private $dbLoadBalancerFactory; |
99 | /** |
100 | * @internal ObjectCacheFactoryTest only |
101 | * @var string |
102 | */ |
103 | public static $localServerCacheClass; |
104 | |
105 | public function __construct( |
106 | ServiceOptions $options, |
107 | StatsFactory $stats, |
108 | Spi $loggerSpi, |
109 | callable $dbLoadBalancerFactory, |
110 | string $domainId |
111 | ) { |
112 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
113 | $this->options = $options; |
114 | $this->stats = $stats; |
115 | $this->logger = $loggerSpi; |
116 | $this->dbLoadBalancerFactory = $dbLoadBalancerFactory; |
117 | $this->domainId = $domainId; |
118 | } |
119 | |
120 | /** |
121 | * Get the default keyspace for this wiki. |
122 | * |
123 | * This is either the value of the MainConfigNames::CachePrefix setting |
124 | * or (if the former is unset) the MainConfigNames::DBname setting, with |
125 | * MainConfigNames::DBprefix (if defined). |
126 | * |
127 | * @return string |
128 | */ |
129 | private function getDefaultKeyspace(): string { |
130 | $cachePrefix = $this->options->get( MainConfigNames::CachePrefix ); |
131 | if ( is_string( $cachePrefix ) && $cachePrefix !== '' ) { |
132 | return $cachePrefix; |
133 | } |
134 | |
135 | return $this->domainId; |
136 | } |
137 | |
138 | /** |
139 | * Create a new cache object of the specified type. |
140 | * |
141 | * @param string|int $id A key in $wgObjectCaches. |
142 | * @return BagOStuff |
143 | */ |
144 | private function newFromId( $id ): BagOStuff { |
145 | if ( $id === CACHE_ANYTHING ) { |
146 | $id = $this->getAnythingId(); |
147 | } |
148 | |
149 | if ( !isset( $this->options->get( MainConfigNames::ObjectCaches )[$id] ) ) { |
150 | // Always recognize these |
151 | if ( $id === CACHE_NONE ) { |
152 | return new EmptyBagOStuff(); |
153 | } elseif ( $id === CACHE_HASH ) { |
154 | return new HashBagOStuff(); |
155 | } elseif ( $id === CACHE_ACCEL ) { |
156 | return self::makeLocalServerCache( $this->getDefaultKeyspace() ); |
157 | } elseif ( $id === 'wincache' ) { |
158 | wfDeprecated( __METHOD__ . ' with cache ID "wincache"', '1.43' ); |
159 | return self::makeLocalServerCache( $this->getDefaultKeyspace() ); |
160 | } |
161 | |
162 | throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " . |
163 | "It is not present in \$wgObjectCaches." ); |
164 | } |
165 | |
166 | return $this->newFromParams( $this->options->get( MainConfigNames::ObjectCaches )[$id] ); |
167 | } |
168 | |
169 | /** |
170 | * Get a cached instance of the specified type of cache object. |
171 | * |
172 | * @param string|int $id A key in $wgObjectCaches. |
173 | * @return BagOStuff |
174 | */ |
175 | public function getInstance( $id ): BagOStuff { |
176 | if ( !isset( $this->instances[$id] ) ) { |
177 | $this->instances[$id] = $this->newFromId( $id ); |
178 | } |
179 | |
180 | return $this->instances[$id]; |
181 | } |
182 | |
183 | /** |
184 | * Create a new cache object from parameters specification supplied. |
185 | * |
186 | * @internal Using this method directly outside of MediaWiki core |
187 | * is discouraged. Use getInstance() instead and supply the ID |
188 | * of the cache instance to be looked up. |
189 | * |
190 | * @param array $params Must have 'factory' or 'class' property. |
191 | * - factory: Callback passed $params that returns BagOStuff. |
192 | * - class: BagOStuff subclass constructed with $params. |
193 | * - loggroup: Alias to set 'logger' key with LoggerFactory group. |
194 | * - .. Other parameters passed to factory or class. |
195 | * |
196 | * @return BagOStuff |
197 | */ |
198 | public function newFromParams( array $params ): BagOStuff { |
199 | $logger = $this->logger->getLogger( $params['loggroup'] ?? 'objectcache' ); |
200 | // Apply default parameters and resolve the logger instance |
201 | $params += [ |
202 | 'logger' => $logger, |
203 | 'keyspace' => $this->getDefaultKeyspace(), |
204 | 'asyncHandler' => [ DeferredUpdates::class, 'addCallableUpdate' ], |
205 | 'reportDupes' => true, |
206 | 'stats' => $this->stats, |
207 | ]; |
208 | |
209 | if ( isset( $params['factory'] ) ) { |
210 | $args = $params['args'] ?? [ $params ]; |
211 | |
212 | return call_user_func( $params['factory'], ...$args ); |
213 | } |
214 | |
215 | if ( !isset( $params['class'] ) ) { |
216 | throw new InvalidArgumentException( |
217 | 'No "factory" nor "class" provided; got "' . print_r( $params, true ) . '"' |
218 | ); |
219 | } |
220 | |
221 | $class = $params['class']; |
222 | |
223 | // Normalization and DI for SqlBagOStuff |
224 | if ( is_a( $class, SqlBagOStuff::class, true ) ) { |
225 | $this->prepareSqlBagOStuffFromParams( $params ); |
226 | } |
227 | |
228 | // Normalization and DI for MemcachedBagOStuff |
229 | if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) { |
230 | $this->prepareMemcachedBagOStuffFromParams( $params ); |
231 | } |
232 | |
233 | // Normalization and DI for MultiWriteBagOStuff |
234 | if ( is_a( $class, MultiWriteBagOStuff::class, true ) ) { |
235 | $this->prepareMultiWriteBagOStuffFromParams( $params ); |
236 | } |
237 | if ( is_a( $class, RESTBagOStuff::class, true ) ) { |
238 | $this->prepareRESTBagOStuffFromParams( $params ); |
239 | } |
240 | |
241 | return new $class( $params ); |
242 | } |
243 | |
244 | private function prepareSqlBagOStuffFromParams( array &$params ): void { |
245 | if ( isset( $params['globalKeyLB'] ) ) { |
246 | throw new InvalidArgumentException( |
247 | 'globalKeyLB in $wgObjectCaches is no longer supported' ); |
248 | } |
249 | if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) { |
250 | $params['servers'] = [ $params['server'] ]; |
251 | unset( $params['server'] ); |
252 | } |
253 | if ( isset( $params['servers'] ) ) { |
254 | // In the past it was not required to set 'dbDirectory' in $wgObjectCaches |
255 | foreach ( $params['servers'] as &$server ) { |
256 | if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) { |
257 | $server['dbDirectory'] = $this->options->get( MainConfigNames::SQLiteDataDir ); |
258 | } |
259 | } |
260 | } elseif ( isset( $params['cluster'] ) ) { |
261 | $cluster = $params['cluster']; |
262 | $dbLbFactory = $this->dbLoadBalancerFactory; |
263 | $params['loadBalancerCallback'] = static function () use ( $cluster, $dbLbFactory ) { |
264 | return $dbLbFactory()->getExternalLB( $cluster ); |
265 | }; |
266 | $params += [ 'dbDomain' => false ]; |
267 | } else { |
268 | $dbLbFactory = $this->dbLoadBalancerFactory; |
269 | $params['loadBalancerCallback'] = static function () use ( $dbLbFactory ) { |
270 | return $dbLbFactory()->getMainLb(); |
271 | }; |
272 | $params += [ 'dbDomain' => false ]; |
273 | } |
274 | $params += [ 'writeBatchSize' => $this->options->get( MainConfigNames::UpdateRowsPerQuery ) ]; |
275 | } |
276 | |
277 | private function prepareMemcachedBagOStuffFromParams( array &$params ): void { |
278 | $params += [ |
279 | 'servers' => $this->options->get( MainConfigNames::MemCachedServers ), |
280 | 'persistent' => $this->options->get( MainConfigNames::MemCachedPersistent ), |
281 | 'timeout' => $this->options->get( MainConfigNames::MemCachedTimeout ), |
282 | ]; |
283 | } |
284 | |
285 | private function prepareMultiWriteBagOStuffFromParams( array &$params ): void { |
286 | // Phan warns about foreach with non-array because it |
287 | // thinks any key can be Closure|IBufferingStatsdDataFactory |
288 | '@phan-var array{caches:array[]} $params'; |
289 | foreach ( $params['caches'] ?? [] as $i => $cacheInfo ) { |
290 | // Ensure logger, keyspace, asyncHandler, etc are injected just as if |
291 | // one of these was configured without MultiWriteBagOStuff (T318272) |
292 | $params['caches'][$i] = $this->newFromParams( $cacheInfo ); |
293 | } |
294 | } |
295 | |
296 | private function prepareRESTBagOStuffFromParams( array &$params ): void { |
297 | $params['telemetry'] = Telemetry::getInstance(); |
298 | } |
299 | |
300 | /** |
301 | * Factory function for CACHE_ACCEL (referenced from configuration) |
302 | * |
303 | * This will look for any APC or APCu style server-local cache. |
304 | * A fallback cache can be specified if none is found. |
305 | * |
306 | * // Direct calls |
307 | * ObjectCache::getLocalServerInstance( $fallbackType ); |
308 | * |
309 | * // From $wgObjectCaches via newFromParams() |
310 | * ObjectCache::getLocalServerInstance( [ 'fallback' => $fallbackType ] ); |
311 | * |
312 | * @param int|string|array $fallback Fallback cache or parameter map with 'fallback' |
313 | * @return BagOStuff |
314 | * @throws InvalidArgumentException |
315 | */ |
316 | public function getLocalServerInstance( $fallback = CACHE_NONE ): BagOStuff { |
317 | $cache = $this->getInstance( CACHE_ACCEL ); |
318 | if ( $cache instanceof EmptyBagOStuff ) { |
319 | if ( is_array( $fallback ) ) { |
320 | $fallback = $fallback['fallback'] ?? CACHE_NONE; |
321 | } |
322 | $cache = $this->getInstance( $fallback ); |
323 | } |
324 | |
325 | return $cache; |
326 | } |
327 | |
328 | /** |
329 | * Clear all the cached instances. |
330 | */ |
331 | public function clear(): void { |
332 | $this->instances = []; |
333 | } |
334 | |
335 | /** |
336 | * Get the class which will be used for the local server cache |
337 | * @return string |
338 | */ |
339 | private static function getLocalServerCacheClass() { |
340 | if ( self::$localServerCacheClass !== null ) { |
341 | return self::$localServerCacheClass; |
342 | } |
343 | if ( function_exists( 'apcu_fetch' ) ) { |
344 | // Make sure the APCu methods actually store anything |
345 | if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) { |
346 | return APCUBagOStuff::class; |
347 | |
348 | } |
349 | } |
350 | |
351 | return EmptyBagOStuff::class; |
352 | } |
353 | |
354 | /** |
355 | * Get the ID that will be used for CACHE_ANYTHING |
356 | * |
357 | * @internal |
358 | * @return string|int |
359 | */ |
360 | public function getAnythingId() { |
361 | $candidates = [ |
362 | $this->options->get( MainConfigNames::MainCacheType ), |
363 | $this->options->get( MainConfigNames::MessageCacheType ), |
364 | $this->options->get( MainConfigNames::ParserCacheType ) |
365 | ]; |
366 | foreach ( $candidates as $candidate ) { |
367 | if ( $candidate === CACHE_ACCEL ) { |
368 | // CACHE_ACCEL might default to nothing if no APCu |
369 | // See includes/ServiceWiring.php |
370 | $class = self::getLocalServerCacheClass(); |
371 | if ( $class !== EmptyBagOStuff::class ) { |
372 | return $candidate; |
373 | } |
374 | } elseif ( $candidate !== CACHE_NONE && $candidate !== CACHE_ANYTHING ) { |
375 | return $candidate; |
376 | } |
377 | } |
378 | |
379 | $services = MediaWikiServices::getInstance(); |
380 | |
381 | if ( $services->isServiceDisabled( 'DBLoadBalancer' ) ) { |
382 | // The DBLoadBalancer service is disabled, so we can't use the database! |
383 | $candidate = CACHE_NONE; |
384 | } elseif ( $services->isStorageDisabled() ) { |
385 | // Storage services are disabled because MediaWikiServices::disableStorage() |
386 | // was called. This is typically the case during installation. |
387 | $candidate = CACHE_NONE; |
388 | } else { |
389 | $candidate = CACHE_DB; |
390 | } |
391 | return $candidate; |
392 | } |
393 | |
394 | /** |
395 | * Create a new BagOStuff instance for local-server caching. |
396 | * |
397 | * Only use this if you explicitly require the creation of |
398 | * a fresh instance. Whenever possible, use or inject the object |
399 | * from MediaWikiServices::getLocalServerObjectCache() instead. |
400 | * |
401 | * NOTE: This method is called very early via Setup.php by ExtensionRegistry, |
402 | * and thus must remain fairly standalone so as to not cause initialization |
403 | * of the MediaWikiServices singleton. |
404 | * |
405 | * @internal For use by ServiceWiring and ExtensionRegistry. There are use |
406 | * cases whereby we want to build up local server cache without service |
407 | * wiring available. |
408 | * @since 1.43, previously on ObjectCache.php since 1.35 |
409 | * |
410 | * @param string $keyspace |
411 | * @return BagOStuff |
412 | */ |
413 | public static function makeLocalServerCache( string $keyspace ) { |
414 | $params = [ |
415 | 'reportDupes' => false, |
416 | // Even simple caches must use a keyspace (T247562) |
417 | 'keyspace' => $keyspace, |
418 | ]; |
419 | $class = self::getLocalServerCacheClass(); |
420 | return new $class( $params ); |
421 | } |
422 | |
423 | /** |
424 | * Determine whether a config ID would access the database |
425 | * |
426 | * @internal For use by ServiceWiring.php |
427 | * @param string|int $id A key in $wgObjectCaches |
428 | * @return bool |
429 | */ |
430 | public function isDatabaseId( $id ) { |
431 | // NOTE: Sanity check if $id is set to CACHE_ANYTHING and |
432 | // everything is going through service wiring. CACHE_ANYTHING |
433 | // would default to CACHE_DB, let's handle that early for cases |
434 | // where all cache configs are set to CACHE_ANYTHING (T362686). |
435 | if ( $id === CACHE_ANYTHING ) { |
436 | $id = $this->getAnythingId(); |
437 | return $this->isDatabaseId( $id ); |
438 | } |
439 | |
440 | if ( !isset( $this->options->get( MainConfigNames::ObjectCaches )[$id] ) ) { |
441 | return false; |
442 | } |
443 | $cache = $this->options->get( MainConfigNames::ObjectCaches )[$id]; |
444 | if ( ( $cache['class'] ?? '' ) === SqlBagOStuff::class ) { |
445 | return true; |
446 | } |
447 | |
448 | return false; |
449 | } |
450 | |
451 | /** |
452 | * Get the main cluster-local cache object. |
453 | * |
454 | * @since 1.43, previously on ObjectCache.php since 1.27 |
455 | * @return BagOStuff |
456 | */ |
457 | public function getLocalClusterInstance() { |
458 | return $this->getInstance( |
459 | $this->options->get( MainConfigNames::MainCacheType ) |
460 | ); |
461 | } |
462 | } |