Cache loading of SettingsBuilder sources
The `SettingsBuilder` now accepts a PSR-16 cache interface with which to store and query settings before attempting to load from each source. By default, no cache is used, but any object that implements the `Psr\SimpleCache\CacheInterface` may be provided to the constructor. An explicit dependency on "psr/simple-cache" has been added to `composer.json`. Note that this dependency already existed in vendor albeit it as a transitive one. An APCu based `SharedMemoryCache` adapter is provided as a canonical PSR-16 compliant interface for production use. Sources are now queued by the `SettingsBuilder` when calling `load()`. If a cache interface has been provided, and the source is considered cacheable (implements `CacheableSource`), then it is wrapped as a `CachedSource` which will query the cache first before loading from the wrapped source. Cache stampedes are mitigated using probabilistic early expiry. The implementation for this was partially based on symfony/cache-contract source code but also from the Wikipedia article and paper referenced therein. See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration Bug: T294748 Change-Id: I52ab3899731546876ee58265bd4a1927886746dc
This commit is contained in:
parent
e00f24a11e
commit
d83a7bcd09
16 changed files with 906 additions and 10 deletions
|
|
@ -43,6 +43,7 @@
|
||||||
"php": ">=7.2.22",
|
"php": ">=7.2.22",
|
||||||
"psr/container": "1.1.1",
|
"psr/container": "1.1.1",
|
||||||
"psr/log": "1.1.4",
|
"psr/log": "1.1.4",
|
||||||
|
"psr/simple-cache": "1.0.1",
|
||||||
"ralouphie/getallheaders": "3.0.3",
|
"ralouphie/getallheaders": "3.0.3",
|
||||||
"symfony/polyfill-php80": "1.23.1",
|
"symfony/polyfill-php80": "1.23.1",
|
||||||
"symfony/yaml": "5.3.6",
|
"symfony/yaml": "5.3.6",
|
||||||
|
|
|
||||||
65
includes/Settings/Cache/ArrayCache.php
Normal file
65
includes/Settings/Cache/ArrayCache.php
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Settings\Cache;
|
||||||
|
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PSR-16 compliant array based cache used for testing.
|
||||||
|
*
|
||||||
|
* @since 1.38
|
||||||
|
*/
|
||||||
|
class ArrayCache implements CacheInterface {
|
||||||
|
/** @var array */
|
||||||
|
private $store = [];
|
||||||
|
|
||||||
|
public function clear(): bool {
|
||||||
|
$this->store = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete( $key ): bool {
|
||||||
|
unset( $this->store[$key] );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteMultiple( $keys ): bool {
|
||||||
|
foreach ( $keys as $key ) {
|
||||||
|
$this->delete( $key );
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get( $key, $default = null ) {
|
||||||
|
if ( !array_key_exists( $key, $this->store ) ) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
return $this->store[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMultiple( $keys, $default = null ) {
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
foreach ( $keys as $key ) {
|
||||||
|
$results[$key] = $this->get( $key, $default );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has( $key ): bool {
|
||||||
|
return array_key_exists( $key, $this->store );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set( $key, $value, $_ttl = null ): bool {
|
||||||
|
$this->store[$key] = $value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMultiple( $values, $ttl = null ): bool {
|
||||||
|
foreach ( $values as $key => $value ) {
|
||||||
|
$this->set( $key, $value, $ttl );
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
includes/Settings/Cache/CacheArgumentException.php
Normal file
12
includes/Settings/Cache/CacheArgumentException.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\SettingsBuilderException;
|
||||||
|
use Psr\SimpleCache\InvalidArgumentException;
|
||||||
|
|
||||||
|
class CacheArgumentException
|
||||||
|
extends SettingsBuilderException
|
||||||
|
implements InvalidArgumentException
|
||||||
|
{
|
||||||
|
}
|
||||||
52
includes/Settings/Cache/CacheableSource.php
Normal file
52
includes/Settings/Cache/CacheableSource.php
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Source\SettingsSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link SettingsSource} that can be cached. It must return a unique
|
||||||
|
* (enough) and deterministic hash key for cache indexing.
|
||||||
|
*
|
||||||
|
* @since 1.38
|
||||||
|
* @todo mark as stable before the 1.38 release
|
||||||
|
*/
|
||||||
|
interface CacheableSource extends SettingsSource {
|
||||||
|
/**
|
||||||
|
* Coefficient used in determining early expiration of cached settings to
|
||||||
|
* avoid stampedes.
|
||||||
|
*
|
||||||
|
* Increasing this value will cause the random early election to happen by
|
||||||
|
* a larger margin of lead time before normal expiry, relative to the
|
||||||
|
* cache value's generation duration. Conversely, returning a lesser value
|
||||||
|
* will narrow the margin of lead time, making the cache hold items for
|
||||||
|
* slightly longer but with more likelihood that concurrent regenerations
|
||||||
|
* and set overwrites will occur. Returning <code>0</code> will
|
||||||
|
* effectively disable early expiration, and by extension disable stampede
|
||||||
|
* mitigation altogether.
|
||||||
|
*
|
||||||
|
* A greater value may be suitable if a source has a highly variable
|
||||||
|
* generation duration, but most implementations should simply return
|
||||||
|
* <code>1.0</code>.
|
||||||
|
*
|
||||||
|
* @link https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
|
||||||
|
* @link https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf
|
||||||
|
*
|
||||||
|
* Optimal Probabilistic Cache Stampede Prevention
|
||||||
|
* Vattani, A.; Chierichetti, F.; Lowenstein, K. (2015), "Optimal
|
||||||
|
* Probabilistic Cache Stampede Prevention" (PDF), Proceedings of the VLDB
|
||||||
|
* Endowment, VLDB, 8 (8): 886–897, doi:10.14778/2757807.2757813, ISSN
|
||||||
|
* 2150-8097 https://cseweb.ucsd.edu/~avattani/papers/cache_stampede.pdf
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getExpiryWeight(): float;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a deterministically computed key for use in caching settings
|
||||||
|
* from this source.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHashKey(): string;
|
||||||
|
}
|
||||||
123
includes/Settings/Cache/CachedSource.php
Normal file
123
includes/Settings/Cache/CachedSource.php
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Source\SettingsSource;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a caching layer for a {@link CacheableSource}.
|
||||||
|
*
|
||||||
|
* @since 1.38
|
||||||
|
* @todo mark as stable before the 1.38 release
|
||||||
|
*/
|
||||||
|
class CachedSource implements SettingsSource {
|
||||||
|
private const DEFAULT_TTL = 60 * 60 * 24;
|
||||||
|
|
||||||
|
/** @var CacheInterface */
|
||||||
|
private $cache;
|
||||||
|
|
||||||
|
/** @var CacheableSource */
|
||||||
|
private $source;
|
||||||
|
|
||||||
|
/** @var float */
|
||||||
|
private $ttl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new CachedSource using an instantiated cache and
|
||||||
|
* {@link CacheableSource}.
|
||||||
|
*
|
||||||
|
* @param CacheInterface $cache
|
||||||
|
* @param CacheableSource $source
|
||||||
|
* @param int $ttl
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
CacheInterface $cache,
|
||||||
|
CacheableSource $source,
|
||||||
|
int $ttl = self::DEFAULT_TTL
|
||||||
|
) {
|
||||||
|
$this->cache = $cache;
|
||||||
|
$this->source = $source;
|
||||||
|
$this->ttl = $ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queries cache for source contents and performs loading/caching of the
|
||||||
|
* source contents on miss.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function load(): array {
|
||||||
|
$key = $this->source->getHashKey();
|
||||||
|
$item = $this->cache->get( $key, null );
|
||||||
|
|
||||||
|
$miss =
|
||||||
|
$item === null ||
|
||||||
|
$this->expiresEarly( $item, $this->source->getExpiryWeight() );
|
||||||
|
|
||||||
|
if ( $miss ) {
|
||||||
|
$item = $this->loadWithMetadata();
|
||||||
|
$this->cache->set( $key, $item );
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shouldn't be possible but let's make phan happy
|
||||||
|
if ( !is_array( $item ) || !array_key_exists( 'value', $item ) ) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $item['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string representation of the encapsulated source.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString(): string {
|
||||||
|
return $this->source->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide whether the cached source should be expired early according to a
|
||||||
|
* probabilistic calculation that becomes more likely as the normal expiry
|
||||||
|
* approaches.
|
||||||
|
*
|
||||||
|
* @param array $item Cached source with expiry metadata.
|
||||||
|
* @param float $weight Coefficient used to increase/decrease the
|
||||||
|
* likelihood of early expiration.
|
||||||
|
*
|
||||||
|
* @link https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function expiresEarly( array $item, float $weight ): bool {
|
||||||
|
if ( !isset( $item['expiry'] ) || !isset( $item['generation'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expiryOffset =
|
||||||
|
$item['generation'] *
|
||||||
|
$weight *
|
||||||
|
log( random_int( 1, PHP_INT_MAX ) / PHP_INT_MAX );
|
||||||
|
|
||||||
|
return ( $item['expiry'] + $expiryOffset ) <= microtime( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps cached source with the metadata needed to perform probabilistic
|
||||||
|
* early expiration to help mitigate cache stampedes.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function loadWithMetadata(): array {
|
||||||
|
$start = microtime( true );
|
||||||
|
$value = $this->source->load();
|
||||||
|
$finish = microtime( true );
|
||||||
|
|
||||||
|
return [
|
||||||
|
'value' => $value,
|
||||||
|
'expiry' => $start + $this->ttl,
|
||||||
|
'generation' => $start - $finish,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
274
includes/Settings/Cache/SharedMemoryCache.php
Normal file
274
includes/Settings/Cache/SharedMemoryCache.php
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Settings\Cache;
|
||||||
|
|
||||||
|
use APCuIterator;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use MediaWiki\Settings\SettingsBuilderException;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
use Traversable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A PSR-16 compliant APCu based shared memory settings cache.
|
||||||
|
*
|
||||||
|
* @since 1.38
|
||||||
|
*/
|
||||||
|
class SharedMemoryCache implements CacheInterface {
|
||||||
|
private const VERSION = '0';
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
private $namespace;
|
||||||
|
|
||||||
|
/** @var int */
|
||||||
|
private $ttl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether APCu is available.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function isSupported() {
|
||||||
|
return function_exists( 'apcu_fetch' )
|
||||||
|
&& filter_var( ini_get( 'apc.enabled' ), FILTER_VALIDATE_BOOLEAN );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $namespace APCu namespace.
|
||||||
|
* @param int $ttl Cached settings TTL in seconds.
|
||||||
|
*
|
||||||
|
* @throws SettingsBuilderException
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $namespace = 'mediawiki.settings',
|
||||||
|
int $ttl = 60 * 60 * 24
|
||||||
|
) {
|
||||||
|
$this->namespace = $namespace;
|
||||||
|
$this->ttl = $ttl;
|
||||||
|
|
||||||
|
if ( !static::isSupported() ) {
|
||||||
|
throw new SettingsBuilderException( 'APCu is not enabled' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all items from this cache.
|
||||||
|
*
|
||||||
|
* @return bool True on success, false on failure.
|
||||||
|
*/
|
||||||
|
public function clear(): bool {
|
||||||
|
return apcu_delete(
|
||||||
|
new APCuIterator(
|
||||||
|
sprintf( '/^%s/', preg_quote( $this->qualifyKey( '' ) ) ),
|
||||||
|
APC_ITER_KEY
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an item from the cache.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return bool True if removed, false if not.
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function delete( $key ): bool {
|
||||||
|
return $this->deleteMultiple( [ $key ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes multiple cache items.
|
||||||
|
*
|
||||||
|
* @param iterable $keys
|
||||||
|
*
|
||||||
|
* @return bool True if all were removed, false if not.
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function deleteMultiple( $keys ): bool {
|
||||||
|
if ( !( is_array( $keys ) || $keys instanceof Traversable ) ) {
|
||||||
|
throw new CacheArgumentException(
|
||||||
|
'given cache $keys is not an iterable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$apcuKeys = [];
|
||||||
|
|
||||||
|
foreach ( $keys as $key ) {
|
||||||
|
$this->assertValidKey( $key );
|
||||||
|
$apcuKeys[] = $this->qualifyKey( $key );
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = apcu_delete( $apcuKeys );
|
||||||
|
|
||||||
|
if ( is_array( $deleted ) ) {
|
||||||
|
return count( $deleted ) === count( $keys );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a value from the cache.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $default
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function get( $key, $default = null ) {
|
||||||
|
$result = $default;
|
||||||
|
|
||||||
|
foreach ( $this->getMultiple( [ $key ], $default ) as $value ) {
|
||||||
|
$result = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches values from the cache.
|
||||||
|
*
|
||||||
|
* @param iterable $keys
|
||||||
|
* @param mixed $default
|
||||||
|
*
|
||||||
|
* @return iterable
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function getMultiple( $keys, $default = null ) {
|
||||||
|
if ( !( is_array( $keys ) || $keys instanceof Traversable ) ) {
|
||||||
|
throw new CacheArgumentException(
|
||||||
|
'given cache $keys is not an iterable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$keyMap = [];
|
||||||
|
|
||||||
|
foreach ( $keys as $key ) {
|
||||||
|
$this->assertValidKey( $key );
|
||||||
|
$keyMap[ $this->qualifyKey( $key ) ] = $key;
|
||||||
|
$results[$key] = $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apcuResults = apcu_fetch( array_keys( $keyMap ), $ok ) ?: [];
|
||||||
|
|
||||||
|
if ( $ok ) {
|
||||||
|
foreach ( $apcuResults as $qualifiedKey => $value ) {
|
||||||
|
$results[ $keyMap[ $qualifiedKey ] ] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for the presence of a given item in the cache.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function has( $key ): bool {
|
||||||
|
$this->assertValidKey( $key );
|
||||||
|
|
||||||
|
return apcu_exists( $this->qualifyKey( $key ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a value in the cache.
|
||||||
|
*
|
||||||
|
* Note that while object values are possible given the underlying APCU
|
||||||
|
* serializer, caching of objects directly should be avoided due to
|
||||||
|
* incompatibilities in serialized representation from PHP version to PHP
|
||||||
|
* version and when simple differences in member scope are made (private
|
||||||
|
* to protected, etc.). Wherever possible, convert objects to arrays
|
||||||
|
* before storing.
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @param mixed $value
|
||||||
|
* @param null|int|DateInterval $ttl Optional. By default, the $ttl
|
||||||
|
* argument passed to the constructor will be used.
|
||||||
|
*
|
||||||
|
* @return bool True on success, false on failure.
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function set( $key, $value, $ttl = null ): bool {
|
||||||
|
return $this->setMultiple( [ $key => $value ], $ttl );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store multiple values in the cache.
|
||||||
|
*
|
||||||
|
* @param iterable $values An iterable of key => value pairs.
|
||||||
|
* @param null|int|DateInterval $ttl Optional. By default, the $ttl
|
||||||
|
* argument passed to the constructor will be used.
|
||||||
|
*
|
||||||
|
* @return bool True on success, false on failure.
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
public function setMultiple( $values, $ttl = null ): bool {
|
||||||
|
if ( $ttl === null ) {
|
||||||
|
$ttl = $this->ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !( is_array( $values ) || $values instanceof Traversable ) ) {
|
||||||
|
throw new CacheArgumentException(
|
||||||
|
'given cache $values is not an iterable'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To be compliant with the PSR-16 interface, we must accept a
|
||||||
|
// DateInterval TTL. Convert it to an int.
|
||||||
|
if ( $ttl instanceof DateInterval ) {
|
||||||
|
$ttl = ( new DateTime( '@0' ) )->add( $ttl )->getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
$apcuValues = [];
|
||||||
|
|
||||||
|
foreach ( $values as $key => $value ) {
|
||||||
|
$this->assertValidKey( $key );
|
||||||
|
$apcuValues[ $this->qualifyKey( $key ) ] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$failures = apcu_store( $apcuValues, null, $ttl );
|
||||||
|
|
||||||
|
return empty( $failures );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualifies a relative key, prefixing it with the internal
|
||||||
|
* namespace and cache implementation version.
|
||||||
|
*
|
||||||
|
* @param string $key Relative key value
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function qualifyKey( string $key ): string {
|
||||||
|
return $this->namespace . '@' . self::VERSION . ':' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
*
|
||||||
|
* @throws CacheArgumentException
|
||||||
|
*/
|
||||||
|
private function assertValidKey( $key ) {
|
||||||
|
if ( !is_string( $key ) ) {
|
||||||
|
throw new CacheArgumentException( 'key must be a string' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $key ) ) {
|
||||||
|
throw new CacheArgumentException( 'key cannot be an empty string' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,26 +2,31 @@
|
||||||
|
|
||||||
namespace MediaWiki\Settings;
|
namespace MediaWiki\Settings;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\CacheableSource;
|
||||||
|
use MediaWiki\Settings\Cache\CachedSource;
|
||||||
use MediaWiki\Settings\Config\ConfigSchemaAggregator;
|
use MediaWiki\Settings\Config\ConfigSchemaAggregator;
|
||||||
use MediaWiki\Settings\Config\ConfigSink;
|
use MediaWiki\Settings\Config\ConfigSink;
|
||||||
use MediaWiki\Settings\Config\PhpIniSink;
|
use MediaWiki\Settings\Config\PhpIniSink;
|
||||||
use MediaWiki\Settings\Source\ArraySource;
|
use MediaWiki\Settings\Source\ArraySource;
|
||||||
use MediaWiki\Settings\Source\FileSource;
|
use MediaWiki\Settings\Source\FileSource;
|
||||||
use MediaWiki\Settings\Source\SettingsSource;
|
use MediaWiki\Settings\Source\SettingsSource;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for loading settings files.
|
* Utility for loading settings files.
|
||||||
* @since 1.38
|
* @since 1.38
|
||||||
*/
|
*/
|
||||||
class SettingsBuilder {
|
class SettingsBuilder {
|
||||||
|
|
||||||
/** @var string */
|
/** @var string */
|
||||||
private $baseDir;
|
private $baseDir;
|
||||||
|
|
||||||
|
/** @var CacheInterface */
|
||||||
|
private $cache;
|
||||||
|
|
||||||
/** @var ConfigSink */
|
/** @var ConfigSink */
|
||||||
private $configSink;
|
private $configSink;
|
||||||
|
|
||||||
/** @var SettingsSource[] */
|
/** @var array */
|
||||||
private $currentBatch;
|
private $currentBatch;
|
||||||
|
|
||||||
/** @var ConfigSchemaAggregator */
|
/** @var ConfigSchemaAggregator */
|
||||||
|
|
@ -34,13 +39,19 @@ class SettingsBuilder {
|
||||||
* @param string $baseDir
|
* @param string $baseDir
|
||||||
* @param ConfigSink $configSink
|
* @param ConfigSink $configSink
|
||||||
* @param PhpIniSink $phpIniSink
|
* @param PhpIniSink $phpIniSink
|
||||||
|
* @param CacheInterface|null $cache PSR-16 compliant cache interface used
|
||||||
|
* to cache settings loaded from each source. The caller should beware
|
||||||
|
* that secrets contained in any source passed to {@link load} or {@link
|
||||||
|
* loadFile} will be cached as well.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $baseDir,
|
string $baseDir,
|
||||||
ConfigSink $configSink,
|
ConfigSink $configSink,
|
||||||
PhpIniSink $phpIniSink
|
PhpIniSink $phpIniSink,
|
||||||
|
CacheInterface $cache = null
|
||||||
) {
|
) {
|
||||||
$this->baseDir = $baseDir;
|
$this->baseDir = $baseDir;
|
||||||
|
$this->cache = $cache;
|
||||||
$this->configSink = $configSink;
|
$this->configSink = $configSink;
|
||||||
$this->configSchema = new ConfigSchemaAggregator();
|
$this->configSchema = new ConfigSchemaAggregator();
|
||||||
$this->phpIniSink = $phpIniSink;
|
$this->phpIniSink = $phpIniSink;
|
||||||
|
|
@ -56,7 +67,12 @@ class SettingsBuilder {
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function load( SettingsSource $source ): self {
|
public function load( SettingsSource $source ): self {
|
||||||
|
if ( $this->cache !== null && $source instanceof CacheableSource ) {
|
||||||
|
$source = new CachedSource( $this->cache, $source );
|
||||||
|
}
|
||||||
|
|
||||||
$this->currentBatch[] = $source;
|
$this->currentBatch[] = $source;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,7 +124,7 @@ class SettingsBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the settings file.
|
* Apply the settings array.
|
||||||
*
|
*
|
||||||
* @param array $settings
|
* @param array $settings
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ namespace MediaWiki\Settings\Source;
|
||||||
* @since 1.38
|
* @since 1.38
|
||||||
*/
|
*/
|
||||||
class ArraySource implements SettingsSource {
|
class ArraySource implements SettingsSource {
|
||||||
|
|
||||||
private $settings;
|
private $settings;
|
||||||
|
|
||||||
public function __construct( array $settings ) {
|
public function __construct( array $settings ) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace MediaWiki\Settings\Source;
|
namespace MediaWiki\Settings\Source;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\CacheableSource;
|
||||||
use MediaWiki\Settings\SettingsBuilderException;
|
use MediaWiki\Settings\SettingsBuilderException;
|
||||||
use MediaWiki\Settings\Source\Format\JsonFormat;
|
use MediaWiki\Settings\Source\Format\JsonFormat;
|
||||||
use MediaWiki\Settings\Source\Format\SettingsFormat;
|
use MediaWiki\Settings\Source\Format\SettingsFormat;
|
||||||
|
|
@ -14,13 +15,26 @@ use Wikimedia\AtEase\AtEase;
|
||||||
*
|
*
|
||||||
* @since 1.38
|
* @since 1.38
|
||||||
*/
|
*/
|
||||||
class FileSource implements SettingsSource {
|
class FileSource implements CacheableSource {
|
||||||
|
|
||||||
private const BUILT_IN_FORMATS = [
|
private const BUILT_IN_FORMATS = [
|
||||||
JsonFormat::class,
|
JsonFormat::class,
|
||||||
YamlFormat::class,
|
YamlFormat::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Early expiry weight. This value influences the margin by which
|
||||||
|
* processes are selected to expire cached local-file settings early to
|
||||||
|
* avoid cache stampedes. Changes to this value are not likely to be
|
||||||
|
* necessary as time spent loading from local files should not have much
|
||||||
|
* variation and should already be well served by the default early expiry
|
||||||
|
* calculation.
|
||||||
|
*
|
||||||
|
* @see getExpiryWeight()
|
||||||
|
* @see CacheableSource::getExpiryWeight()
|
||||||
|
*/
|
||||||
|
private const EXPIRY_WEIGHT = 1.0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format to use for reading the file, if given.
|
* Format to use for reading the file, if given.
|
||||||
*
|
*
|
||||||
|
|
@ -98,6 +112,35 @@ class FileSource implements SettingsSource {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coefficient used in determining early expiration of cached settings to
|
||||||
|
* avoid stampedes.
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
public function getExpiryWeight(): float {
|
||||||
|
return self::EXPIRY_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a hash key computed from the file's inode, size, and last
|
||||||
|
* modified timestamp.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getHashKey(): string {
|
||||||
|
$stat = stat( $this->path );
|
||||||
|
|
||||||
|
if ( $stat === false ) {
|
||||||
|
throw new SettingsBuilderException(
|
||||||
|
"Failed to stat file '{path}'",
|
||||||
|
[ 'path' => $this->path ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf( '%x-%x-%x', $stat['ino'], $stat['size'], $stat['mtime'] );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns this file source as a string.
|
* Returns this file source as a string.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,9 @@ $wgAutoloadClasses += [
|
||||||
'MediaWiki\Tests\Unit\Revision\RevisionSlotsTest' => "$testDir/phpunit/unit/includes/Revision/RevisionSlotsTest.php",
|
'MediaWiki\Tests\Unit\Revision\RevisionSlotsTest' => "$testDir/phpunit/unit/includes/Revision/RevisionSlotsTest.php",
|
||||||
'MediaWiki\Tests\Unit\Revision\RevisionStoreRecordTest' => "$testDir/phpunit/unit/includes/Revision/RevisionStoreRecordTest.php",
|
'MediaWiki\Tests\Unit\Revision\RevisionStoreRecordTest' => "$testDir/phpunit/unit/includes/Revision/RevisionStoreRecordTest.php",
|
||||||
|
|
||||||
|
# tests/phpunit/unit/includes/Settings/Cache
|
||||||
|
'MediaWiki\Tests\Unit\Settings\Cache\CacheInterfaceTestTrait' => "$testDir/phpunit/unit/includes/Settings/Cache/CacheInterfaceTestTrait.php",
|
||||||
|
|
||||||
# tests/phpunit/unit/includes/Settings/Config
|
# tests/phpunit/unit/includes/Settings/Config
|
||||||
'MediaWiki\Tests\Unit\Settings\Config\ConfigSinkTestTrait' => "$testDir/phpunit/unit/includes/Settings/Config/ConfigSinkTestTrait.php",
|
'MediaWiki\Tests\Unit\Settings\Config\ConfigSinkTestTrait' => "$testDir/phpunit/unit/includes/Settings/Config/ConfigSinkTestTrait.php",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Tests\Unit\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\ArrayCache;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \MediaWiki\Settings\Cache\ArrayCache
|
||||||
|
*/
|
||||||
|
class ArrayCacheTest extends TestCase {
|
||||||
|
use CacheInterfaceTestTrait;
|
||||||
|
|
||||||
|
protected function newCache(): ArrayCache {
|
||||||
|
return new ArrayCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Tests\Unit\Settings\Cache;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
|
trait CacheInterfaceTestTrait {
|
||||||
|
abstract protected function newCache(): CacheInterface;
|
||||||
|
|
||||||
|
public function testSetAndGet() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$cache->set( 'one', 'fish1' );
|
||||||
|
$cache->set( 'two', 'fish2', 60 );
|
||||||
|
$cache->set( 'red', 'fish3', DateInterval::createFromDateString( '@60' ) );
|
||||||
|
|
||||||
|
$this->assertSame( 'fish1', $cache->get( 'one' ) );
|
||||||
|
$this->assertSame( 'fish2', $cache->get( 'two' ) );
|
||||||
|
$this->assertSame( 'fish3', $cache->get( 'red' ) );
|
||||||
|
$this->assertSame( 'fish4', $cache->get( 'blue', 'fish4' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetAndGetMultiple() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$cache->setMultiple(
|
||||||
|
[
|
||||||
|
'one' => 'fish1',
|
||||||
|
'two' => 'fish2',
|
||||||
|
'red' => 'fish3',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[
|
||||||
|
'one' => 'fish1',
|
||||||
|
'two' => 'fish2',
|
||||||
|
'red' => 'fish3',
|
||||||
|
'blue' => 'fish4',
|
||||||
|
],
|
||||||
|
$cache->getMultiple( [ 'one', 'two', 'red', 'blue' ], 'fish4' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteAndHas() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$cache->set( 'red', 'fish' );
|
||||||
|
$cache->set( 'blue', 'fish' );
|
||||||
|
|
||||||
|
$cache->delete( 'red' );
|
||||||
|
|
||||||
|
$this->assertFalse( $cache->has( 'red' ) );
|
||||||
|
$this->assertTrue( $cache->has( 'blue' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeleteMultiple() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$cache->set( 'one', 'fish' );
|
||||||
|
$cache->set( 'two', 'fish' );
|
||||||
|
$cache->set( 'red', 'fish' );
|
||||||
|
$cache->set( 'blue', 'fish' );
|
||||||
|
|
||||||
|
$cache->deleteMultiple( [ 'two', 'red' ] );
|
||||||
|
|
||||||
|
$this->assertTrue( $cache->has( 'one' ) );
|
||||||
|
$this->assertFalse( $cache->has( 'two' ) );
|
||||||
|
$this->assertFalse( $cache->has( 'red' ) );
|
||||||
|
$this->assertTrue( $cache->has( 'blue' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testClear() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$cache->set( 'one', 'fish' );
|
||||||
|
$cache->set( 'two', 'fish' );
|
||||||
|
$cache->set( 'red', 'fish' );
|
||||||
|
$cache->set( 'blue', 'fish' );
|
||||||
|
|
||||||
|
$this->assertTrue( $cache->has( 'one' ) );
|
||||||
|
$this->assertTrue( $cache->has( 'two' ) );
|
||||||
|
$this->assertTrue( $cache->has( 'red' ) );
|
||||||
|
$this->assertTrue( $cache->has( 'blue' ) );
|
||||||
|
|
||||||
|
$cache->clear();
|
||||||
|
|
||||||
|
$this->assertFalse( $cache->has( 'one' ) );
|
||||||
|
$this->assertFalse( $cache->has( 'two' ) );
|
||||||
|
$this->assertFalse( $cache->has( 'red' ) );
|
||||||
|
$this->assertFalse( $cache->has( 'blue' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Tests\Unit\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\ArrayCache;
|
||||||
|
use MediaWiki\Settings\Cache\CacheableSource;
|
||||||
|
use MediaWiki\Settings\Cache\CachedSource;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \MediaWiki\Settings\Cache\CachedSource
|
||||||
|
*/
|
||||||
|
class CachedSourceTest extends TestCase {
|
||||||
|
public function testLoadWithMiss() {
|
||||||
|
$cache = new ArrayCache();
|
||||||
|
$source = $this->createMock( CacheableSource::class );
|
||||||
|
$cacheSource = new CachedSource( $cache, $source );
|
||||||
|
|
||||||
|
$settings = [ 'config' => [ 'Foo' => 'value' ] ];
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'getHashKey' )
|
||||||
|
->willReturn( 'abc123' );
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'load' )
|
||||||
|
->willReturn( $settings );
|
||||||
|
|
||||||
|
$this->assertSame( $settings, $cacheSource->load() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadWithHit() {
|
||||||
|
$cache = new ArrayCache();
|
||||||
|
$source = $this->createMock( CacheableSource::class );
|
||||||
|
$cacheSource = new CachedSource( $cache, $source );
|
||||||
|
|
||||||
|
$settings = [ 'config' => [ 'Foo' => 'value' ] ];
|
||||||
|
|
||||||
|
$cache->set( 'abc123', [ 'value' => $settings ] );
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'getHashKey' )
|
||||||
|
->willReturn( 'abc123' );
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->never() )
|
||||||
|
->method( 'load' );
|
||||||
|
|
||||||
|
$this->assertSame( $settings, $cacheSource->load() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadWithNoEarlyExpiry() {
|
||||||
|
$cache = new ArrayCache();
|
||||||
|
$source = $this->createMock( CacheableSource::class );
|
||||||
|
$cacheSource = new CachedSource( $cache, $source );
|
||||||
|
|
||||||
|
$settings = [ 'config' => [ 'Foo' => 'value' ] ];
|
||||||
|
|
||||||
|
$cache->set(
|
||||||
|
'abc123',
|
||||||
|
[
|
||||||
|
'value' => $settings,
|
||||||
|
'expiry' => microtime( true ) + 60,
|
||||||
|
'generation' => 1.0
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'getHashKey' )
|
||||||
|
->willReturn( 'abc123' );
|
||||||
|
|
||||||
|
// Effectively disable early expiry for the test since it's
|
||||||
|
// probabilistic and thus cannot be tested reliably without more
|
||||||
|
// dependency injection, but at least ensure that the code path is
|
||||||
|
// exercised.
|
||||||
|
$source
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'getExpiryWeight' )
|
||||||
|
->willReturn( 0.0 );
|
||||||
|
|
||||||
|
$source
|
||||||
|
->expects( $this->never() )
|
||||||
|
->method( 'load' );
|
||||||
|
|
||||||
|
$this->assertSame( $settings, $cacheSource->load() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MediaWiki\Tests\Unit\Settings\Cache;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\CacheArgumentException;
|
||||||
|
use MediaWiki\Settings\Cache\SharedMemoryCache;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \MediaWiki\Settings\Cache\SharedMemoryCache
|
||||||
|
*/
|
||||||
|
class SharedMemoryCacheTest extends TestCase {
|
||||||
|
use CacheInterfaceTestTrait;
|
||||||
|
|
||||||
|
protected function newCache(): SharedMemoryCache {
|
||||||
|
return new SharedMemoryCache( 'phpunit.mediawiki.settings' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUp(): void {
|
||||||
|
if ( !SharedMemoryCache::isSupported() || !ini_get( 'apc.enable_cli' ) ) {
|
||||||
|
$this->markTestSkipped(
|
||||||
|
'skipped shared memory cache tests. set apc.enable_cli=on to run them'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tearDown(): void {
|
||||||
|
if ( SharedMemoryCache::isSupported() && ini_get( 'apc.enable_cli' ) ) {
|
||||||
|
$this->newCache()->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetWithZeroLengthKey() {
|
||||||
|
$cache = $this->newCache();
|
||||||
|
|
||||||
|
$this->expectException( CacheArgumentException::class );
|
||||||
|
|
||||||
|
$cache->set( '', 'foo' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNamespacing() {
|
||||||
|
$cache1 = new SharedMemoryCache( 'phpunit.mediawiki.settings.one' );
|
||||||
|
$cache2 = new SharedMemoryCache( 'phpunit.mediawiki.settings.two' );
|
||||||
|
|
||||||
|
$cache1->set( 'one', 'fish' );
|
||||||
|
$cache1->set( 'two', 'fish' );
|
||||||
|
$cache2->set( 'red', 'fish' );
|
||||||
|
$cache2->set( 'blue', 'fish' );
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cache1->clear();
|
||||||
|
|
||||||
|
$this->assertFalse( $cache1->has( 'one' ) );
|
||||||
|
$this->assertFalse( $cache1->has( 'two' ) );
|
||||||
|
|
||||||
|
$this->assertTrue( $cache2->has( 'red' ) );
|
||||||
|
$this->assertTrue( $cache2->has( 'blue' ) );
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
$cache2->clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace phpunit\unit\includes\Settings;
|
namespace phpunit\unit\includes\Settings;
|
||||||
|
|
||||||
|
use MediaWiki\Settings\Cache\CacheableSource;
|
||||||
use MediaWiki\Settings\Config\ArrayConfigBuilder;
|
use MediaWiki\Settings\Config\ArrayConfigBuilder;
|
||||||
use MediaWiki\Settings\Config\ConfigSink;
|
use MediaWiki\Settings\Config\ConfigSink;
|
||||||
use MediaWiki\Settings\Config\MergeStrategy;
|
use MediaWiki\Settings\Config\MergeStrategy;
|
||||||
|
|
@ -9,6 +10,7 @@ use MediaWiki\Settings\Config\PhpIniSink;
|
||||||
use MediaWiki\Settings\SettingsBuilder;
|
use MediaWiki\Settings\SettingsBuilder;
|
||||||
use MediaWiki\Settings\SettingsBuilderException;
|
use MediaWiki\Settings\SettingsBuilderException;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\SimpleCache\CacheInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @covers \MediaWiki\Settings\SettingsBuilder
|
* @covers \MediaWiki\Settings\SettingsBuilder
|
||||||
|
|
@ -18,16 +20,19 @@ class SettingsBuilderTest extends TestCase {
|
||||||
/**
|
/**
|
||||||
* @param ConfigSink|null $configBuilder
|
* @param ConfigSink|null $configBuilder
|
||||||
* @param PhpIniSink|null $phpIniSink
|
* @param PhpIniSink|null $phpIniSink
|
||||||
|
* @param CacheInterface|null $cache
|
||||||
* @return SettingsBuilder
|
* @return SettingsBuilder
|
||||||
*/
|
*/
|
||||||
private function newSettingsBuilder(
|
private function newSettingsBuilder(
|
||||||
ConfigSink $configBuilder = null,
|
ConfigSink $configBuilder = null,
|
||||||
PhpIniSink $phpIniSink = null
|
PhpIniSink $phpIniSink = null,
|
||||||
|
CacheInterface $cache = null
|
||||||
): SettingsBuilder {
|
): SettingsBuilder {
|
||||||
return new SettingsBuilder(
|
return new SettingsBuilder(
|
||||||
__DIR__,
|
__DIR__,
|
||||||
$configBuilder ?? new ArrayConfigBuilder(),
|
$configBuilder ?? new ArrayConfigBuilder(),
|
||||||
$phpIniSink ?? new PhpIniSink()
|
$phpIniSink ?? new PhpIniSink(),
|
||||||
|
$cache
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,8 +191,8 @@ class SettingsBuilderTest extends TestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testApplyDefaultDoesNotOverwriteExisting() {
|
public function testApplyDefaultDoesNotOverwriteExisting() {
|
||||||
$configBuilder = ( new ArrayConfigBuilder() )
|
$configBuilder = new ArrayConfigBuilder();
|
||||||
->set( 'MySetting', 'existing' );
|
$configBuilder->set( 'MySetting', 'existing' );
|
||||||
$this->newSettingsBuilder( $configBuilder )
|
$this->newSettingsBuilder( $configBuilder )
|
||||||
->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default' ], ], ] )
|
->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'default' ], ], ] )
|
||||||
->apply();
|
->apply();
|
||||||
|
|
@ -201,4 +206,34 @@ class SettingsBuilderTest extends TestCase {
|
||||||
->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'override' ], ], ] )
|
->loadArray( [ 'config-schema' => [ 'MySetting' => [ 'default' => 'override' ], ], ] )
|
||||||
->apply();
|
->apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testLoadsCacheableSource() {
|
||||||
|
$mockSource = $this->createMock( CacheableSource::class );
|
||||||
|
$mockCache = $this->createMock( CacheInterface::class );
|
||||||
|
$configBuilder = new ArrayConfigBuilder();
|
||||||
|
$builder = $this
|
||||||
|
->newSettingsBuilder( $configBuilder, null, $mockCache )
|
||||||
|
->load( $mockSource );
|
||||||
|
|
||||||
|
// Mock a cache miss
|
||||||
|
$mockSource
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'getHashKey' )
|
||||||
|
->willReturn( 'abc123' );
|
||||||
|
|
||||||
|
$mockCache
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'get' )
|
||||||
|
->with( 'abc123' )
|
||||||
|
->willReturn( null );
|
||||||
|
|
||||||
|
$mockSource
|
||||||
|
->expects( $this->once() )
|
||||||
|
->method( 'load' )
|
||||||
|
->willReturn( [ 'config' => [ 'MySetting' => 'BlaBla' ] ] );
|
||||||
|
|
||||||
|
$builder->apply();
|
||||||
|
|
||||||
|
$this->assertSame( 'BlaBla', $configBuilder->build()->get( 'MySetting' ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,12 @@ class FileSourceTest extends TestCase {
|
||||||
|
|
||||||
$settings = $source->load();
|
$settings = $source->load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetHashKey() {
|
||||||
|
$source = new FileSource( __DIR__ . '/fixtures/settings.json' );
|
||||||
|
|
||||||
|
// We can't reliably mock the filesystem stat so simply ensure the
|
||||||
|
// method returns and is non-zero in length
|
||||||
|
$this->assertNotEmpty( $source->getHashKey() );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue