wiki.techinc.nl/includes/Settings/Cache/SharedMemoryCache.php
Dan Duvall d83a7bcd09 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
2021-11-29 12:54:59 -08:00

274 lines
6.1 KiB
PHP

<?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' );
}
}
}