Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
69.92% |
93 / 133 |
|
46.67% |
7 / 15 |
CRAP | |
0.00% |
0 / 1 |
ObjectCacheFactory | |
69.92% |
93 / 133 |
|
46.67% |
7 / 15 |
133.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
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 | |
83.33% |
20 / 24 |
|
0.00% |
0 / 1 |
6.17 | |||
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 | |||
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\Logger\Spi; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\MediaWikiServices; |
26 | use Wikimedia\ObjectCache\APCUBagOStuff; |
27 | use Wikimedia\ObjectCache\BagOStuff; |
28 | use Wikimedia\ObjectCache\EmptyBagOStuff; |
29 | use Wikimedia\ObjectCache\HashBagOStuff; |
30 | use Wikimedia\ObjectCache\MemcachedBagOStuff; |
31 | use Wikimedia\ObjectCache\MultiWriteBagOStuff; |
32 | use Wikimedia\Stats\StatsFactory; |
33 | use Wikimedia\Telemetry\TracerInterface; |
34 | |
35 | /** |
36 | * Factory for cache objects as configured in the ObjectCaches setting. |
37 | * |
38 | * The word "cache" has two main dictionary meanings, and both |
39 | * are used in this factory class. They are: |
40 | * |
41 | * - a) Cache (the computer science definition). |
42 | * A place to store copies or computations on existing data for |
43 | * higher access speeds. |
44 | * - b) Storage. |
45 | * A place to store lightweight data that is not canonically |
46 | * stored anywhere else (e.g. a "hoard" of objects). |
47 | * |
48 | * Primary entry points: |
49 | * |
50 | * - ObjectCacheFactory::getLocalServerInstance( $fallbackType ) |
51 | * Purpose: Memory cache for very hot keys. |
52 | * Stored only on the individual web server (typically APC or APCu for web requests, |
53 | * and EmptyBagOStuff in CLI mode). |
54 | * Not replicated to the other servers. |
55 | * |
56 | * - ObjectCacheFactory::getLocalClusterInstance() |
57 | * Purpose: Memory storage for per-cluster coordination and tracking. |
58 | * A typical use case would be a rate limit counter or cache regeneration mutex. |
59 | * Stored centrally within the local data-center. Not replicated to other DCs. |
60 | * Configured by $wgMainCacheType. |
61 | * |
62 | * - ObjectCacheFactory::getInstance( $cacheType ) |
63 | * Purpose: Special cases (like tiered memory/disk caches). |
64 | * Get a specific cache type by key in $wgObjectCaches. |
65 | * |
66 | * All the above BagOStuff cache instances have their makeKey() |
67 | * method scoped to the *current* wiki ID. Use makeGlobalKey() to avoid this scoping |
68 | * when using keys that need to be shared amongst wikis. |
69 | * |
70 | * @ingroup Cache |
71 | * @since 1.42 |
72 | */ |
73 | class ObjectCacheFactory { |
74 | /** |
75 | * @internal For use by ServiceWiring.php |
76 | */ |
77 | public const CONSTRUCTOR_OPTIONS = [ |
78 | MainConfigNames::SQLiteDataDir, |
79 | MainConfigNames::UpdateRowsPerQuery, |
80 | MainConfigNames::MemCachedServers, |
81 | MainConfigNames::MemCachedPersistent, |
82 | MainConfigNames::MemCachedTimeout, |
83 | MainConfigNames::CachePrefix, |
84 | MainConfigNames::ObjectCaches, |
85 | MainConfigNames::MainCacheType, |
86 | MainConfigNames::MessageCacheType, |
87 | MainConfigNames::ParserCacheType, |
88 | ]; |
89 | |
90 | private ServiceOptions $options; |
91 | private StatsFactory $stats; |
92 | private Spi $logger; |
93 | private TracerInterface $telemetry; |
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 | TracerInterface $telemetry |
112 | ) { |
113 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
114 | $this->options = $options; |
115 | $this->stats = $stats; |
116 | $this->logger = $loggerSpi; |
117 | $this->dbLoadBalancerFactory = $dbLoadBalancerFactory; |
118 | $this->domainId = $domainId; |
119 | $this->telemetry = $telemetry; |
120 | } |
121 | |
122 | /** |
123 | * Get the default keyspace for this wiki. |
124 | * |
125 | * This is either the value of the MainConfigNames::CachePrefix setting |
126 | * or (if the former is unset) the MainConfigNames::DBname setting, with |
127 | * MainConfigNames::DBprefix (if defined). |
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 | 'telemetry' => $this->telemetry, |
208 | ]; |
209 | |
210 | if ( isset( $params['factory'] ) ) { |
211 | $args = $params['args'] ?? [ $params ]; |
212 | |
213 | return $params['factory']( ...$args ); |
214 | } |
215 | |
216 | if ( !isset( $params['class'] ) ) { |
217 | throw new InvalidArgumentException( |
218 | 'No "factory" nor "class" provided; got "' . print_r( $params, true ) . '"' |
219 | ); |
220 | } |
221 | |
222 | $class = $params['class']; |
223 | |
224 | // Normalization and DI for SqlBagOStuff |
225 | if ( is_a( $class, SqlBagOStuff::class, true ) ) { |
226 | $this->prepareSqlBagOStuffFromParams( $params ); |
227 | } |
228 | |
229 | // Normalization and DI for MemcachedBagOStuff |
230 | if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) { |
231 | $this->prepareMemcachedBagOStuffFromParams( $params ); |
232 | } |
233 | |
234 | // Normalization and DI for MultiWriteBagOStuff |
235 | if ( is_a( $class, MultiWriteBagOStuff::class, true ) ) { |
236 | $this->prepareMultiWriteBagOStuffFromParams( $params ); |
237 | } |
238 | |
239 | return new $class( $params ); |
240 | } |
241 | |
242 | private function prepareSqlBagOStuffFromParams( array &$params ): void { |
243 | if ( isset( $params['globalKeyLB'] ) ) { |
244 | throw new InvalidArgumentException( |
245 | 'globalKeyLB in $wgObjectCaches is no longer supported' ); |
246 | } |
247 | if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) { |
248 | $params['servers'] = [ $params['server'] ]; |
249 | unset( $params['server'] ); |
250 | } |
251 | if ( isset( $params['servers'] ) ) { |
252 | // In the past it was not required to set 'dbDirectory' in $wgObjectCaches |
253 | foreach ( $params['servers'] as &$server ) { |
254 | if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) { |
255 | $server['dbDirectory'] = $this->options->get( MainConfigNames::SQLiteDataDir ); |
256 | } |
257 | } |
258 | } elseif ( isset( $params['cluster'] ) ) { |
259 | $cluster = $params['cluster']; |
260 | $dbLbFactory = $this->dbLoadBalancerFactory; |
261 | $params['loadBalancerCallback'] = static function () use ( $cluster, $dbLbFactory ) { |
262 | return $dbLbFactory()->getExternalLB( $cluster ); |
263 | }; |
264 | $params += [ 'dbDomain' => false ]; |
265 | } else { |
266 | $dbLbFactory = $this->dbLoadBalancerFactory; |
267 | $params['loadBalancerCallback'] = static function () use ( $dbLbFactory ) { |
268 | return $dbLbFactory()->getMainLb(); |
269 | }; |
270 | $params += [ 'dbDomain' => false ]; |
271 | } |
272 | $params += [ 'writeBatchSize' => $this->options->get( MainConfigNames::UpdateRowsPerQuery ) ]; |
273 | } |
274 | |
275 | private function prepareMemcachedBagOStuffFromParams( array &$params ): void { |
276 | $params += [ |
277 | 'servers' => $this->options->get( MainConfigNames::MemCachedServers ), |
278 | 'persistent' => $this->options->get( MainConfigNames::MemCachedPersistent ), |
279 | 'timeout' => $this->options->get( MainConfigNames::MemCachedTimeout ), |
280 | ]; |
281 | } |
282 | |
283 | private function prepareMultiWriteBagOStuffFromParams( array &$params ): void { |
284 | // Phan warns about foreach with non-array because it |
285 | // thinks any key can be Closure|IBufferingStatsdDataFactory |
286 | '@phan-var array{caches:array[]} $params'; |
287 | foreach ( $params['caches'] ?? [] as $i => $cacheInfo ) { |
288 | // Ensure logger, keyspace, asyncHandler, etc are injected just as if |
289 | // one of these was configured without MultiWriteBagOStuff (T318272) |
290 | $params['caches'][$i] = $this->newFromParams( $cacheInfo ); |
291 | } |
292 | } |
293 | |
294 | /** |
295 | * Factory function for CACHE_ACCEL (referenced from configuration) |
296 | * |
297 | * This will look for any APC or APCu style server-local cache. |
298 | * A fallback cache can be specified if none is found. |
299 | * |
300 | * // Direct calls |
301 | * ObjectCache::getLocalServerInstance( $fallbackType ); |
302 | * |
303 | * // From $wgObjectCaches via newFromParams() |
304 | * ObjectCache::getLocalServerInstance( [ 'fallback' => $fallbackType ] ); |
305 | * |
306 | * @param int|string|array $fallback Fallback cache or parameter map with 'fallback' |
307 | * @return BagOStuff |
308 | * @throws InvalidArgumentException |
309 | */ |
310 | public function getLocalServerInstance( $fallback = CACHE_NONE ): BagOStuff { |
311 | $cache = $this->getInstance( CACHE_ACCEL ); |
312 | if ( $cache instanceof EmptyBagOStuff ) { |
313 | if ( is_array( $fallback ) ) { |
314 | $fallback = $fallback['fallback'] ?? CACHE_NONE; |
315 | } |
316 | $cache = $this->getInstance( $fallback ); |
317 | } |
318 | |
319 | return $cache; |
320 | } |
321 | |
322 | /** |
323 | * Clear all the cached instances. |
324 | */ |
325 | public function clear(): void { |
326 | $this->instances = []; |
327 | } |
328 | |
329 | /** |
330 | * Get the class which will be used for the local server cache |
331 | * @return string |
332 | */ |
333 | private static function getLocalServerCacheClass() { |
334 | if ( self::$localServerCacheClass !== null ) { |
335 | return self::$localServerCacheClass; |
336 | } |
337 | if ( function_exists( 'apcu_fetch' ) ) { |
338 | // Make sure the APCu methods actually store anything |
339 | if ( PHP_SAPI !== 'cli' || ini_get( 'apc.enable_cli' ) ) { |
340 | return APCUBagOStuff::class; |
341 | |
342 | } |
343 | } |
344 | |
345 | return EmptyBagOStuff::class; |
346 | } |
347 | |
348 | /** |
349 | * Get the ID that will be used for CACHE_ANYTHING |
350 | * |
351 | * @internal |
352 | * @return string|int |
353 | */ |
354 | public function getAnythingId() { |
355 | $candidates = [ |
356 | $this->options->get( MainConfigNames::MainCacheType ), |
357 | $this->options->get( MainConfigNames::MessageCacheType ), |
358 | $this->options->get( MainConfigNames::ParserCacheType ) |
359 | ]; |
360 | foreach ( $candidates as $candidate ) { |
361 | if ( $candidate === CACHE_ACCEL ) { |
362 | // CACHE_ACCEL might default to nothing if no APCu |
363 | // See includes/ServiceWiring.php |
364 | $class = self::getLocalServerCacheClass(); |
365 | if ( $class !== EmptyBagOStuff::class ) { |
366 | return $candidate; |
367 | } |
368 | } elseif ( $candidate !== CACHE_NONE && $candidate !== CACHE_ANYTHING ) { |
369 | return $candidate; |
370 | } |
371 | } |
372 | |
373 | $services = MediaWikiServices::getInstance(); |
374 | |
375 | if ( $services->isServiceDisabled( 'DBLoadBalancer' ) ) { |
376 | // The DBLoadBalancer service is disabled, so we can't use the database! |
377 | $candidate = CACHE_NONE; |
378 | } elseif ( $services->isStorageDisabled() ) { |
379 | // Storage services are disabled because MediaWikiServices::disableStorage() |
380 | // was called. This is typically the case during installation. |
381 | $candidate = CACHE_NONE; |
382 | } else { |
383 | $candidate = CACHE_DB; |
384 | } |
385 | return $candidate; |
386 | } |
387 | |
388 | /** |
389 | * Create a new BagOStuff instance for local-server caching. |
390 | * |
391 | * Only use this if you explicitly require the creation of |
392 | * a fresh instance. Whenever possible, use or inject the object |
393 | * from MediaWikiServices::getLocalServerObjectCache() instead. |
394 | * |
395 | * NOTE: This method is called very early via Setup.php by ExtensionRegistry, |
396 | * and thus must remain fairly standalone so as to not cause initialization |
397 | * of the MediaWikiServices singleton. |
398 | * |
399 | * @internal For use by ServiceWiring and ExtensionRegistry. There are use |
400 | * cases whereby we want to build up local server cache without service |
401 | * wiring available. |
402 | * @since 1.43, previously on ObjectCache.php since 1.35 |
403 | * |
404 | * @param string $keyspace |
405 | * @return BagOStuff |
406 | */ |
407 | public static function makeLocalServerCache( string $keyspace ) { |
408 | $params = [ |
409 | 'reportDupes' => false, |
410 | // Even simple caches must use a keyspace (T247562) |
411 | 'keyspace' => $keyspace, |
412 | ]; |
413 | $class = self::getLocalServerCacheClass(); |
414 | return new $class( $params ); |
415 | } |
416 | |
417 | /** |
418 | * Determine whether a config ID would access the database |
419 | * |
420 | * @internal For use by ServiceWiring.php |
421 | * @param string|int $id A key in $wgObjectCaches |
422 | * @return bool |
423 | */ |
424 | public function isDatabaseId( $id ) { |
425 | // NOTE: Sanity check if $id is set to CACHE_ANYTHING and |
426 | // everything is going through service wiring. CACHE_ANYTHING |
427 | // would default to CACHE_DB, let's handle that early for cases |
428 | // where all cache configs are set to CACHE_ANYTHING (T362686). |
429 | if ( $id === CACHE_ANYTHING ) { |
430 | $id = $this->getAnythingId(); |
431 | return $this->isDatabaseId( $id ); |
432 | } |
433 | |
434 | if ( !isset( $this->options->get( MainConfigNames::ObjectCaches )[$id] ) ) { |
435 | return false; |
436 | } |
437 | $cache = $this->options->get( MainConfigNames::ObjectCaches )[$id]; |
438 | if ( ( $cache['class'] ?? '' ) === SqlBagOStuff::class ) { |
439 | return true; |
440 | } |
441 | |
442 | return false; |
443 | } |
444 | |
445 | /** |
446 | * Get the main cluster-local cache object. |
447 | * |
448 | * @since 1.43, previously on ObjectCache.php since 1.27 |
449 | * @return BagOStuff |
450 | */ |
451 | public function getLocalClusterInstance() { |
452 | return $this->getInstance( |
453 | $this->options->get( MainConfigNames::MainCacheType ) |
454 | ); |
455 | } |
456 | } |