diff options
Diffstat (limited to 'includes/objectcache')
-rw-r--r-- | includes/objectcache/ObjectCache.php | 209 | ||||
-rw-r--r-- | includes/objectcache/ObjectCacheFactory.php | 295 |
2 files changed, 331 insertions, 173 deletions
diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 91ea0a57daee..129bb46d1268 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -21,12 +21,7 @@ * @ingroup Cache */ -use MediaWiki\Deferred\DeferredUpdates; -use MediaWiki\Http\Telemetry; -use MediaWiki\Logger\LoggerFactory; -use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; -use MediaWiki\WikiMap\WikiMap; /** * Functions to get cache objects @@ -66,7 +61,10 @@ use MediaWiki\WikiMap\WikiMap; * @ingroup Cache */ class ObjectCache { - /** @var BagOStuff[] Map of (id => BagOStuff) */ + /** + * @deprecated Use ObjectCacheFactory instead. + * @var BagOStuff[] Map of (id => BagOStuff) + */ public static $instances = []; /** @@ -78,157 +76,26 @@ class ObjectCache { /** * Get a cached instance of the specified type of cache object. * - * @param string|int $id A key in $wgObjectCaches. - * @return BagOStuff - */ - public static function getInstance( $id ) { - if ( !isset( self::$instances[$id] ) ) { - self::$instances[$id] = self::newFromId( $id ); - } - - return self::$instances[$id]; - } - - /** - * Create a new cache object of the specified type. + * @deprecated Use ObjectCacheFactory::getInstance instead. * * @param string|int $id A key in $wgObjectCaches. * @return BagOStuff - * @throws InvalidArgumentException */ - private static function newFromId( $id ) { - global $wgObjectCaches; - - if ( !isset( $wgObjectCaches[$id] ) ) { - // Always recognize these ones - if ( $id === CACHE_NONE ) { - return new EmptyBagOStuff(); - } elseif ( $id === CACHE_HASH ) { - return new HashBagOStuff(); - } - - throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " . - "It is not present in \$wgObjectCaches." ); - } - - return self::newFromParams( $wgObjectCaches[$id] ); + public static function getInstance( $id ) { + return MediaWikiServices::getInstance()->getObjectCacheFactory()->getInstance( $id ); } /** - * Get the default keyspace for this wiki. + * @see ObjectCacheFactory::newFromParams() * - * This is either the value of the `CachePrefix` configuration variable, - * or (if the former is unset) the `DBname` configuration variable, with - * `DBprefix` (if defined). - * - * @return string - */ - private static function getDefaultKeyspace() { - global $wgCachePrefix; - - $keyspace = $wgCachePrefix; - if ( is_string( $keyspace ) && $keyspace !== '' ) { - return $keyspace; - } - - return WikiMap::getCurrentWikiDbDomain()->getId(); - } - - /** - * Create a new cache object from parameters. + * @deprecated since 1.42, Use ObjectCacheFactory::newFromParams instead. + * @param array $params * - * @param array $params Must have 'factory' or 'class' property. - * - factory: Callback passed $params that returns BagOStuff. - * - class: BagOStuff subclass constructed with $params. - * - loggroup: Alias to set 'logger' key with LoggerFactory group. - * - .. Other parameters passed to factory or class. - * @param MediaWikiServices|null $services [internal] * @return BagOStuff */ - public static function newFromParams( array $params, MediaWikiServices $services = null ) { - $services ??= MediaWikiServices::getInstance(); - $conf = $services->getMainConfig(); - - // Apply default parameters and resolve the logger instance - $params += [ - 'logger' => LoggerFactory::getInstance( $params['loggroup'] ?? 'objectcache' ), - 'keyspace' => self::getDefaultKeyspace(), - 'asyncHandler' => [ DeferredUpdates::class, 'addCallableUpdate' ], - 'reportDupes' => true, - 'stats' => $services->getStatsFactory(), - ]; - - if ( isset( $params['factory'] ) ) { - $args = $params['args'] ?? [ $params ]; - - return call_user_func( $params['factory'], ...$args ); - } - - if ( !isset( $params['class'] ) ) { - throw new InvalidArgumentException( - 'No "factory" nor "class" provided; got "' . print_r( $params, true ) . '"' - ); - } - - $class = $params['class']; - - // Normalization and DI for SqlBagOStuff - if ( is_a( $class, SqlBagOStuff::class, true ) ) { - if ( isset( $params['globalKeyLB'] ) ) { - throw new InvalidArgumentException( - 'globalKeyLB in $wgObjectCaches is no longer supported' ); - } - if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) { - $params['servers'] = [ $params['server'] ]; - unset( $params['server'] ); - } - if ( isset( $params['servers'] ) ) { - // In the past it was not required to set 'dbDirectory' in $wgObjectCaches - foreach ( $params['servers'] as &$server ) { - if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) { - $server['dbDirectory'] = $conf->get( MainConfigNames::SQLiteDataDir ); - } - } - } elseif ( isset( $params['cluster'] ) ) { - $cluster = $params['cluster']; - $params['loadBalancerCallback'] = static function () use ( $services, $cluster ) { - return $services->getDBLoadBalancerFactory()->getExternalLB( $cluster ); - }; - $params += [ 'dbDomain' => false ]; - } else { - $params['loadBalancerCallback'] = static function () use ( $services ) { - return $services->getDBLoadBalancer(); - }; - $params += [ 'dbDomain' => false ]; - } - $params += [ 'writeBatchSize' => $conf->get( MainConfigNames::UpdateRowsPerQuery ) ]; - } - - // Normalization and DI for MemcachedBagOStuff - if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) { - $params += [ - 'servers' => $conf->get( MainConfigNames::MemCachedServers ), - 'persistent' => $conf->get( MainConfigNames::MemCachedPersistent ), - 'timeout' => $conf->get( MainConfigNames::MemCachedTimeout ), - ]; - } - - // Normalization and DI for MultiWriteBagOStuff - if ( is_a( $class, MultiWriteBagOStuff::class, true ) ) { - // Phan warns about foreach with non-array because it - // thinks any key can be Closure|IBufferingStatsdDataFactory - '@phan-var array{caches:array[]} $params'; - foreach ( $params['caches'] ?? [] as $i => $cacheInfo ) { - // Ensure logger, keyspace, asyncHandler, etc are injected just as if - // one of these was configured without MultiWriteBagOStuff. - $params['caches'][$i] = self::newFromParams( $cacheInfo, $services ); - } - } - if ( is_a( $class, RESTBagOStuff::class, true ) ) { - $params['telemetry'] = Telemetry::getInstance(); - } - - return new $class( $params ); + public static function newFromParams( array $params ) { + return MediaWikiServices::getInstance()->getObjectCacheFactory() + ->newFromParams( $params ); } /** @@ -241,18 +108,23 @@ class ObjectCache { * If no cache choice is configured (by default $wgMainCacheType is CACHE_NONE), * then CACHE_ANYTHING will forward to CACHE_DB. * - * @param array $params + * @deprecated since 1.42, + * Use ObjectCacheFactory::newInstance( ObjectCache::getAnythingId() ); + * * @return BagOStuff */ - public static function newAnything( $params ) { - return self::getInstance( self::getAnythingId() ); + public static function newAnything() { + return MediaWikiServices::getInstance()->getObjectCacheFactory() + ->getInstance( self::getAnythingId() ); } /** + * @internal Used by ObjectCacheFactory and ObjectCache. + * * Get the ID that will be used for CACHE_ANYTHING * @return string|int */ - private static function getAnythingId() { + public static function getAnythingId() { global $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType; $candidates = [ $wgMainCacheType, $wgMessageCacheType, $wgParserCacheType ]; foreach ( $candidates as $candidate ) { @@ -284,16 +156,8 @@ class ObjectCache { } /** - * Factory function for CACHE_ACCEL (referenced from configuration) - * - * This will look for any APC or APCu style server-local cache. - * A fallback cache can be specified if none is found. - * - * // Direct calls - * ObjectCache::getLocalServerInstance( $fallbackType ); - * - * // From $wgObjectCaches via newFromParams() - * ObjectCache::getLocalServerInstance( [ 'fallback' => $fallbackType ] ); + * @deprecated since 1.42, Use ObjectCacheFactory::getLocaServerInstance() + * @see ObjectCacheFactory::getLocalServerInstance() * * @param int|string|array $fallback Fallback cache or parameter map with 'fallback' * @return BagOStuff @@ -301,15 +165,8 @@ class ObjectCache { * @since 1.27 */ public static function getLocalServerInstance( $fallback = CACHE_NONE ) { - $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); - if ( $cache instanceof EmptyBagOStuff ) { - if ( is_array( $fallback ) ) { - $fallback = $fallback['fallback'] ?? CACHE_NONE; - } - $cache = self::getInstance( $fallback ); - } - - return $cache; + return MediaWikiServices::getInstance()->getObjectCacheFactory() + ->getLocalServerInstance( $fallback ); } /** @@ -337,7 +194,7 @@ class ObjectCache { if ( ( $cache['class'] ?? '' ) === SqlBagOStuff::class ) { return true; } - if ( ( $cache['factory'] ?? '' ) === 'ObjectCache::newAnything' ) { + if ( $id === CACHE_ANYTHING ) { $id = self::getAnythingId(); return self::isDatabaseId( $id ); } @@ -345,10 +202,12 @@ class ObjectCache { } /** + * @deprecated since 1.42, Use ObjectCacheFactory::clear() instead. + * * Clear all the cached instances. */ public static function clear() { - self::$instances = []; + MediaWikiServices::getInstance()->getObjectCacheFactory()->clear(); } /** @@ -362,14 +221,18 @@ class ObjectCache { * and thus must remain fairly standalone so as to not cause initialization * of the MediaWikiServices singleton. * + * @internal For use by ServiceWiring and ExtensionRegistry. There are use + * cases whereby we want to build up local server cache without service + * wiring available. * @since 1.35 + * @param string|false $cachePrefix * @return BagOStuff */ - public static function makeLocalServerCache(): BagOStuff { + public static function makeLocalServerCache( $cachePrefix ): BagOStuff { $params = [ 'reportDupes' => false, // Even simple caches must use a keyspace (T247562) - 'keyspace' => self::getDefaultKeyspace(), + 'keyspace' => $cachePrefix, ]; $class = self::getLocalServerCacheClass(); return new $class( $params ); diff --git a/includes/objectcache/ObjectCacheFactory.php b/includes/objectcache/ObjectCacheFactory.php new file mode 100644 index 000000000000..ebcbda18003e --- /dev/null +++ b/includes/objectcache/ObjectCacheFactory.php @@ -0,0 +1,295 @@ +<?php +/** + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +use MediaWiki\Config\ServiceOptions; +use MediaWiki\Http\Telemetry; +use MediaWiki\Logger\Spi; +use MediaWiki\MainConfigNames; +use MediaWiki\MediaWikiServices; +use MediaWiki\WikiMap\WikiMap; +use Wikimedia\Stats\StatsFactory; + +/** + * Factory for cache objects as configured in the ObjectCaches setting. + * + * @ingroup Cache + * @since 1.42 + */ +class ObjectCacheFactory { + /** + * @internal For use by ServiceWiring.php + * @var array + */ + public const CONSTRUCTOR_OPTIONS = [ + MainConfigNames::SQLiteDataDir, + MainConfigNames::UpdateRowsPerQuery, + MainConfigNames::MemCachedServers, + MainConfigNames::MemCachedPersistent, + MainConfigNames::MemCachedTimeout, + MainConfigNames::CachePrefix, + MainConfigNames::ObjectCaches, + MainConfigNames::MainCacheType, + ]; + + private ServiceOptions $options; + private StatsFactory $stats; + private Spi $logger; + /** @var BagOStuff[] */ + private $instances = []; + + public function __construct( + ServiceOptions $options, + StatsFactory $stats, + Spi $loggerSpi + ) { + $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); + $this->options = $options; + $this->stats = $stats; + $this->logger = $loggerSpi; + } + + /** + * Get the default keyspace for this wiki. + * + * This is either the value of the `CachePrefix` configuration variable, + * or (if the former is unset) the `DBname` configuration variable, with + * `DBprefix` (if defined). + * + * @return string + */ + private function getDefaultKeyspace(): string { + $cachePrefix = $this->options->get( MainConfigNames::CachePrefix ); + if ( is_string( $cachePrefix ) && $cachePrefix !== '' ) { + return $cachePrefix; + } + + return WikiMap::getCurrentWikiDbDomain()->getId(); + } + + /** + * Create a new cache object of the specified type. + * + * @param string|int $id A key in $wgObjectCaches. + * @return BagOStuff + * @throws InvalidArgumentException + */ + private function newFromId( $id ): BagOStuff { + if ( $id === CACHE_ANYTHING ) { + $id = ObjectCache::getAnythingId(); + } + + if ( !isset( $this->options->get( MainConfigNames::ObjectCaches )[$id] ) ) { + // Always recognize these ones + if ( $id === CACHE_NONE ) { + return new EmptyBagOStuff(); + } elseif ( $id === CACHE_HASH ) { + return new HashBagOStuff(); + } elseif ( $id === CACHE_ACCEL ) { + return ObjectCache::makeLocalServerCache( + $this->options->get( MainConfigNames::CachePrefix ) + ); + } + + throw new InvalidArgumentException( "Invalid object cache type \"$id\" requested. " . + "It is not present in \$wgObjectCaches." ); + } + + return $this->newFromParams( $this->options->get( MainConfigNames::ObjectCaches )[$id] ); + } + + /** + * Get a cached instance of the specified type of cache object. + * + * @param string|int $id A key in $wgObjectCaches. + * @return BagOStuff + */ + public function getInstance( $id ): BagOStuff { + if ( !isset( $this->instances[$id] ) ) { + $this->instances[$id] = $this->newFromId( $id ); + } + + return $this->instances[$id]; + } + + /** + * @internal Using this method directly outside of MediaWiki core + * is discouraged. Use getInstance() instead and supply the ID + * of the cache instance to be looked up. + * + * Create a new cache object from parameters specification supplied. + * + * @param array $params Must have 'factory' or 'class' property. + * - factory: Callback passed $params that returns BagOStuff. + * - class: BagOStuff subclass constructed with $params. + * - loggroup: Alias to set 'logger' key with LoggerFactory group. + * - .. Other parameters passed to factory or class. + * + * @return BagOStuff + */ + public function newFromParams( array $params ): BagOStuff { + $logger = $this->logger->getLogger( $params['loggroup'] ?? 'objectcache' ); + // Apply default parameters and resolve the logger instance + $params += [ + 'logger' => $logger, + 'keyspace' => $this->getDefaultKeyspace(), + 'asyncHandler' => [ DeferredUpdates::class, 'addCallableUpdate' ], + 'reportDupes' => true, + 'stats' => $this->stats, + ]; + + if ( isset( $params['factory'] ) ) { + $args = $params['args'] ?? [ $params ]; + + return call_user_func( $params['factory'], ...$args ); + } + + if ( !isset( $params['class'] ) ) { + throw new InvalidArgumentException( + 'No "factory" nor "class" provided; got "' . print_r( $params, true ) . '"' + ); + } + + $class = $params['class']; + + // Normalization and DI for SqlBagOStuff + if ( is_a( $class, SqlBagOStuff::class, true ) ) { + $this->prepareSqlBagOStuffFromParams( $params ); + } + + // Normalization and DI for MemcachedBagOStuff + if ( is_subclass_of( $class, MemcachedBagOStuff::class ) ) { + $this->prepareMemcachedBagOStuffFromParams( $params ); + } + + // Normalization and DI for MultiWriteBagOStuff + if ( is_a( $class, MultiWriteBagOStuff::class, true ) ) { + $this->prepareMultiWriteBagOStuffFromParams( $params ); + } + if ( is_a( $class, RESTBagOStuff::class, true ) ) { + $this->prepareRESTBagOStuffFromParams( $params ); + } + + return new $class( $params ); + } + + private function prepareSqlBagOStuffFromParams( array &$params ): void { + if ( isset( $params['globalKeyLB'] ) ) { + throw new InvalidArgumentException( + 'globalKeyLB in $wgObjectCaches is no longer supported' ); + } + if ( isset( $params['server'] ) && !isset( $params['servers'] ) ) { + $params['servers'] = [ $params['server'] ]; + unset( $params['server'] ); + } + if ( isset( $params['servers'] ) ) { + // In the past it was not required to set 'dbDirectory' in $wgObjectCaches + foreach ( $params['servers'] as &$server ) { + if ( $server['type'] === 'sqlite' && !isset( $server['dbDirectory'] ) ) { + $server['dbDirectory'] = $this->options->get( MainConfigNames::SQLiteDataDir ); + } + } + } elseif ( isset( $params['cluster'] ) ) { + $cluster = $params['cluster']; + $params['loadBalancerCallback'] = static function () use ( $cluster ) { + return MediaWikiServices::getInstance()->getDBLoadBalancerFactory() + ->getExternalLB( $cluster ); + }; + $params += [ 'dbDomain' => false ]; + } else { + $params['loadBalancerCallback'] = static function () { + return MediaWikiServices::getInstance()->getDBLoadBalancer(); + }; + $params += [ 'dbDomain' => false ]; + } + $params += [ 'writeBatchSize' => $this->options->get( MainConfigNames::UpdateRowsPerQuery ) ]; + } + + private function prepareMemcachedBagOStuffFromParams( array &$params ): void { + $params += [ + 'servers' => $this->options->get( MainConfigNames::MemCachedServers ), + 'persistent' => $this->options->get( MainConfigNames::MemCachedPersistent ), + 'timeout' => $this->options->get( MainConfigNames::MemCachedTimeout ), + ]; + } + + private function prepareMultiWriteBagOStuffFromParams( array &$params ): void { + // Phan warns about foreach with non-array because it + // thinks any key can be Closure|IBufferingStatsdDataFactory + '@phan-var array{caches:array[]} $params'; + foreach ( $params['caches'] ?? [] as $i => $cacheInfo ) { + // Ensure logger, keyspace, asyncHandler, etc are injected just as if + // one of these was configured without MultiWriteBagOStuff. + $params['caches'][$i] = $this->newFromParams( $cacheInfo ); + } + } + + private function prepareRESTBagOStuffFromParams( array &$params ): void { + $params['telemetry'] = Telemetry::getInstance(); + } + + /** + * Factory function for CACHE_ACCEL (referenced from configuration) + * + * This will look for any APC or APCu style server-local cache. + * A fallback cache can be specified if none is found. + * + * // Direct calls + * ObjectCache::getLocalServerInstance( $fallbackType ); + * + * // From $wgObjectCaches via newFromParams() + * ObjectCache::getLocalServerInstance( [ 'fallback' => $fallbackType ] ); + * + * @param int|string|array $fallback Fallback cache or parameter map with 'fallback' + * @return BagOStuff + * @throws InvalidArgumentException + */ + public function getLocalServerInstance( $fallback = CACHE_NONE ): BagOStuff { + $cache = $this->getInstance( CACHE_ACCEL ); + if ( $cache instanceof EmptyBagOStuff ) { + if ( is_array( $fallback ) ) { + $fallback = $fallback['fallback'] ?? CACHE_NONE; + } + $cache = $this->getInstance( $fallback ); + } + + return $cache; + } + + /** + * Clear all the cached instances. + */ + public function clear(): void { + $this->instances = []; + } + + /** + * @internal For tests ONLY. + * + * @param string|int $cacheId + * @param BagOStuff $cache + * @return void + */ + public function setInstanceForTesting( $cacheId, BagOStuff $cache ): void { + if ( !defined( 'MW_PHPUNIT_TEST' ) ) { + throw new LogicException( __METHOD__ . ' can not be called outside of tests' ); + } + $this->instances[$cacheId] = $cache; + } +} |