Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
40.66% |
37 / 91 |
|
33.33% |
4 / 12 |
CRAP | |
0.00% |
0 / 1 |
ObjectCacheFactory | |
40.66% |
37 / 91 |
|
33.33% |
4 / 12 |
339.73 | |
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 | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
8.60 | |||
getInstance | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromParams | |
64.00% |
16 / 25 |
|
0.00% |
0 / 1 |
9.29 | |||
prepareSqlBagOStuffFromParams | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
90 | |||
prepareMemcachedBagOStuffFromParams | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
prepareMultiWriteBagOStuffFromParams | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
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 | |||
setInstanceForTesting | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
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\Http\Telemetry; |
23 | use MediaWiki\Logger\Spi; |
24 | use MediaWiki\MainConfigNames; |
25 | use Wikimedia\Stats\StatsFactory; |
26 | |
27 | /** |
28 | * Factory for cache objects as configured in the ObjectCaches setting. |
29 | * |
30 | * The word "cache" has two main dictionary meanings, and both |
31 | * are used in this factory class. They are: |
32 | * |
33 | * - a) Cache (the computer science definition). |
34 | * A place to store copies or computations on existing data for |
35 | * higher access speeds. |
36 | * - b) Storage. |
37 | * A place to store lightweight data that is not canonically |
38 | * stored anywhere else (e.g. a "hoard" of objects). |
39 | * |
40 | * Primary entry points: |
41 | * |
42 | * - ObjectCacheFactory::getLocalServerInstance( $fallbackType ) |
43 | * Purpose: Memory cache for very hot keys. |
44 | * Stored only on the individual web server (typically APC or APCu for web requests, |
45 | * and EmptyBagOStuff in CLI mode). |
46 | * Not replicated to the other servers. |
47 | * |
48 | * - ObjectCacheFactory::getInstance( $cacheType ) |
49 | * Purpose: Special cases (like tiered memory/disk caches). |
50 | * Get a specific cache type by key in $wgObjectCaches. |
51 | * |
52 | * All the above BagOStuff cache instances have their makeKey() |
53 | * method scoped to the *current* wiki ID. Use makeGlobalKey() to avoid this scoping |
54 | * when using keys that need to be shared amongst wikis. |
55 | * |
56 | * @ingroup Cache |
57 | * @since 1.42 |
58 | */ |
59 | class ObjectCacheFactory { |
60 | /** |
61 | * @internal For use by ServiceWiring.php |
62 | * @var array |
63 | */ |
64 | public const CONSTRUCTOR_OPTIONS = [ |
65 | MainConfigNames::SQLiteDataDir, |
66 | MainConfigNames::UpdateRowsPerQuery, |
67 | MainConfigNames::MemCachedServers, |
68 | MainConfigNames::MemCachedPersistent, |
69 | MainConfigNames::MemCachedTimeout, |
70 | MainConfigNames::CachePrefix, |
71 | MainConfigNames::ObjectCaches, |
72 | MainConfigNames::MainCacheType, |
73 | ]; |
74 | |
75 | private ServiceOptions $options; |
76 | private StatsFactory $stats; |
77 | private Spi $logger; |
78 | /** @var BagOStuff[] */ |
79 | private $instances = []; |
80 | private string $domainId; |
81 | /** @var callable */ |
82 | private $dbLoadBalancerFactory; |
83 | |
84 | public function __construct( |
85 | ServiceOptions $options, |
86 | StatsFactory $stats, |
87 | Spi $loggerSpi, |
88 | callable $dbLoadBalancerFactory, |
89 | string $domainId |
90 | ) { |
91 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
92 | $this->options = $options; |
93 | $this->stats = $stats; |
94 | $this->logger = $loggerSpi; |
95 | $this->dbLoadBalancerFactory = $dbLoadBalancerFactory; |
96 | $this->domainId = $domainId; |
97 | } |
98 | |
99 | /** |
100 | * Get the default keyspace for this wiki. |
101 | * |
102 | * This is either the value of the MainConfigNames::CachePrefix setting |
103 | * or (if the former is unset) the MainConfigNames::DBname setting, with |
104 | * MainConfigNames::DBprefix (if defined). |
105 | * |
106 | * @return string |
107 | */ |
108 | private function getDefaultKeyspace(): string { |
109 | $cachePrefix = $this->options->get( MainConfigNames::CachePrefix ); |
110 | if ( is_string( $cachePrefix ) && $cachePrefix !== '' ) { |
111 | return $cachePrefix; |
112 | } |
113 | |
114 | return $this->domainId; |
115 | } |
116 | |
117 | /** |
118 | * Create a new cache object of the specified type. |
119 | * |
120 | * @param string|int $id A key in $wgObjectCaches. |
121 | * @return BagOStuff |
122 | */ |
123 | private function newFromId( $id ): BagOStuff { |
124 | if ( $id === CACHE_ANYTHING ) { |
125 | $id = ObjectCache::getAnythingId(); |
126 | } |
127 | |
128 | if ( !isset( $this->options->get( MainConfigNames::ObjectCaches )[$id] ) ) { |
129 | // Always recognize these |
130 | if ( $id === CACHE_NONE ) { |
131 | return new EmptyBagOStuff(); |
132 | } elseif ( $id === CACHE_HASH ) { |
133 | return new HashBagOStuff(); |
134 | } elseif ( $id === CACHE_ACCEL ) { |
135 | return ObjectCache::makeLocalServerCache( $this->getDefaultKeyspace() ); |
136 | } |
137 | |
138 | throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " . |
139 | "It is not present in \$wgObjectCaches." ); |
140 | } |
141 | |
142 | return $this->newFromParams( $this->options->get( MainConfigNames::ObjectCaches )[$id] ); |
143 | } |
144 | |
145 | /** |
146 | * Get a cached instance of the specified type of cache object. |
147 | * |
148 | * @param string|int $id A key in $wgObjectCaches. |
149 | * @return BagOStuff |
150 | */ |
151 | public function getInstance( $id ): BagOStuff { |
152 | if ( !isset( $this->instances[$id] ) ) { |
153 | $this->instances[$id] = $this->newFromId( $id ); |
154 | } |
155 | |
156 | return $this->instances[$id]; |
157 | } |
158 | |
159 | /** |
160 | * @internal Using this method directly outside of MediaWiki core |
161 | * is discouraged. Use getInstance() instead and supply the ID |
162 | * of the cache instance to be looked up. |
163 | * |
164 | * Create a new cache object from parameters specification supplied. |
165 | * |
166 | * @param array $params Must have 'factory' or 'class' property. |
167 | * - factory: Callback passed $params that returns BagOStuff. |
168 | * - class: BagOStuff subclass constructed with $params. |
169 | * - loggroup: Alias to set 'logger' key with LoggerFactory group. |
170 | * - .. Other parameters passed to factory or class. |
171 | * |
172 | * @return BagOStuff |
173 | */ |
174 | public function newFromParams( array $params ): BagOStuff { |
175 | $logger = $this->logger->getLogger( $params['loggroup'] ?? 'objectcache' ); |
176 | // Apply default parameters and resolve the logger instance |
177 | $params += [ |
178 | 'logger' => $logger, |
179 | 'keyspace' => $this->getDefaultKeyspace(), |
180 | 'asyncHandler' => [ DeferredUpdates::class, 'addCallableUpdate' ], |
181 | 'reportDupes' => true, |
182 | 'stats' => $this->stats, |
183 | ]; |
184 | |
185 | if ( isset( $params['factory'] ) ) { |
186 | $args = $params['args'] ?? [ $params ]; |
187 | |
188 | return call_user_func( $params['factory'], ...$args ); |
189 | } |
190 | |
191 | if ( !isset( $params['class'] ) ) { |
192 | throw new InvalidArgumentException( |
193 | 'No "factory" nor "class" provided; got "' . print_r( $params, true ) . '"' |
194 | ); |
195 | } |
196 | |
197 | $class = $params['class']; |
198 | |
199 | // Normalization and DI for SqlBagOStuff |
200 | if ( is_a( $class, SqlBagOStuff::class, true ) ) { |
201 | $this->prepareSqlBagOStuffFromParams( $params ); |
202 | } |
203 | |
204 | // Normalization and DI for MemcachedBagOStuff |
205 | if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) { |
206 | $this->prepareMemcachedBagOStuffFromParams( $params ); |
207 | } |
208 | |
209 | // Normalization and DI for MultiWriteBagOStuff |
210 | if ( is_a( $class, MultiWriteBagOStuff::class, true ) ) { |
211 | $this->prepareMultiWriteBagOStuffFromParams( $params ); |
212 | } |
213 | if ( is_a( $class, RESTBagOStuff::class, true ) ) { |
214 | $this->prepareRESTBagOStuffFromParams( $params ); |
215 | } |
216 | |
217 | return new $class( $params ); |
218 | } |
219 | |
220 | private function prepareSqlBagOStuffFromParams( array &$params ): void { |
221 | if ( isset( $params['globalKeyLB'] ) ) { |
222 | throw new InvalidArgumentException( |
223 | 'globalKeyLB in $wgObjectCaches is no longer supported' ); |
224 | } |
225 | if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) { |
226 | $params['servers'] = [ $params['server'] ]; |
227 | unset( $params['server'] ); |
228 | } |
229 | if ( isset( $params['servers'] ) ) { |
230 | // In the past it was not required to set 'dbDirectory' in $wgObjectCaches |
231 | foreach ( $params['servers'] as &$server ) { |
232 | if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) { |
233 | $server['dbDirectory'] = $this->options->get( MainConfigNames::SQLiteDataDir ); |
234 | } |
235 | } |
236 | } elseif ( isset( $params['cluster'] ) ) { |
237 | $cluster = $params['cluster']; |
238 | $dbLbFactory = $this->dbLoadBalancerFactory; |
239 | $params['loadBalancerCallback'] = static function () use ( $cluster, $dbLbFactory ) { |
240 | return $dbLbFactory()->getExternalLB( $cluster ); |
241 | }; |
242 | $params += [ 'dbDomain' => false ]; |
243 | } else { |
244 | $dbLbFactory = $this->dbLoadBalancerFactory; |
245 | $params['loadBalancerCallback'] = static function () use ( $dbLbFactory ) { |
246 | return $dbLbFactory()->getMainLb(); |
247 | }; |
248 | $params += [ 'dbDomain' => false ]; |
249 | } |
250 | $params += [ 'writeBatchSize' => $this->options->get( MainConfigNames::UpdateRowsPerQuery ) ]; |
251 | } |
252 | |
253 | private function prepareMemcachedBagOStuffFromParams( array &$params ): void { |
254 | $params += [ |
255 | 'servers' => $this->options->get( MainConfigNames::MemCachedServers ), |
256 | 'persistent' => $this->options->get( MainConfigNames::MemCachedPersistent ), |
257 | 'timeout' => $this->options->get( MainConfigNames::MemCachedTimeout ), |
258 | ]; |
259 | } |
260 | |
261 | private function prepareMultiWriteBagOStuffFromParams( array &$params ): void { |
262 | // Phan warns about foreach with non-array because it |
263 | // thinks any key can be Closure|IBufferingStatsdDataFactory |
264 | '@phan-var array{caches:array[]} $params'; |
265 | foreach ( $params['caches'] ?? [] as $i => $cacheInfo ) { |
266 | // Ensure logger, keyspace, asyncHandler, etc are injected just as if |
267 | // one of these was configured without MultiWriteBagOStuff. |
268 | $params['caches'][$i] = $this->newFromParams( $cacheInfo ); |
269 | } |
270 | } |
271 | |
272 | private function prepareRESTBagOStuffFromParams( array &$params ): void { |
273 | $params['telemetry'] = Telemetry::getInstance(); |
274 | } |
275 | |
276 | /** |
277 | * Factory function for CACHE_ACCEL (referenced from configuration) |
278 | * |
279 | * This will look for any APC or APCu style server-local cache. |
280 | * A fallback cache can be specified if none is found. |
281 | * |
282 | * // Direct calls |
283 | * ObjectCache::getLocalServerInstance( $fallbackType ); |
284 | * |
285 | * // From $wgObjectCaches via newFromParams() |
286 | * ObjectCache::getLocalServerInstance( [ 'fallback' => $fallbackType ] ); |
287 | * |
288 | * @param int|string|array $fallback Fallback cache or parameter map with 'fallback' |
289 | * @return BagOStuff |
290 | * @throws InvalidArgumentException |
291 | */ |
292 | public function getLocalServerInstance( $fallback = CACHE_NONE ): BagOStuff { |
293 | $cache = $this->getInstance( CACHE_ACCEL ); |
294 | if ( $cache instanceof EmptyBagOStuff ) { |
295 | if ( is_array( $fallback ) ) { |
296 | $fallback = $fallback['fallback'] ?? CACHE_NONE; |
297 | } |
298 | $cache = $this->getInstance( $fallback ); |
299 | } |
300 | |
301 | return $cache; |
302 | } |
303 | |
304 | /** |
305 | * Clear all the cached instances. |
306 | */ |
307 | public function clear(): void { |
308 | $this->instances = []; |
309 | } |
310 | |
311 | /** |
312 | * @internal For tests ONLY. |
313 | * |
314 | * @param string|int $cacheId |
315 | * @param BagOStuff $cache |
316 | * @return void |
317 | */ |
318 | public function setInstanceForTesting( $cacheId, BagOStuff $cache ): void { |
319 | if ( !defined( 'MW_PHPUNIT_TEST' ) ) { |
320 | throw new LogicException( __METHOD__ . ' can not be called outside of tests' ); |
321 | } |
322 | $this->instances[$cacheId] = $cache; |
323 | } |
324 | } |