Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.66% covered (danger)
40.66%
37 / 91
33.33% covered (danger)
33.33%
4 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ObjectCacheFactory
40.66% covered (danger)
40.66%
37 / 91
33.33% covered (danger)
33.33%
4 / 12
339.73
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultKeyspace
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 newFromId
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
8.60
 getInstance
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newFromParams
64.00% covered (warning)
64.00%
16 / 25
0.00% covered (danger)
0.00%
0 / 1
9.29
 prepareSqlBagOStuffFromParams
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 prepareMemcachedBagOStuffFromParams
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 prepareMultiWriteBagOStuffFromParams
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 prepareRESTBagOStuffFromParams
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalServerInstance
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 clear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setInstanceForTesting
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
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
21use MediaWiki\Config\ServiceOptions;
22use MediaWiki\Http\Telemetry;
23use MediaWiki\Logger\Spi;
24use MediaWiki\MainConfigNames;
25use 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 */
59class 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}