Instead of telling ExternalStoreMedium classes the default write stores and using
that to make them read-only, let them be configured via other means. For example,
ExternalStoreMwstore already respects FileBackend::isReadOnly() for each location
(e.g. file backends) and ExternalStoreDB checks LoadBalancer::getReadOnlyMode()
for each location (e.g. DB cluster).
Make ExternalStoreAccess::isReadOnly() take a list of base URLs, default to the
default write stores if not specified.
Bug: T227156
Change-Id: I3161890fb2ccb46d6206628f0cd88f8af9f1688c
Follows-Up: I40c3b5534fc8a31116c4c5eb64ee6e4903a6197a
*
* @param string $data
* @param array $params Map of context parameters; same as ExternalStoreFactory::getStore()
- * @param string[]|null $tryStores Refer to $wgDefaultExternalStore
+ * @param string[]|null $tryStores Base URLs to try, e.g. [ "DB://cluster1" ]
* @return string|bool The URL of the stored data item, or false on error
* @throws ExternalStoreException
*/
}
/**
+ * @param string[]|string|null $storeUrls Base URL(s) to check, e.g. [ "DB://cluster1" ]
* @return bool Whether all the default insertion stores are marked as read-only
* @throws ExternalStoreException
*/
- public function isReadOnly() {
- $writableStores = $this->storeFactory->getWriteBaseUrls();
- if ( !$writableStores ) {
+ public function isReadOnly( $storeUrls = null ) {
+ if ( $storeUrls === null ) {
+ $storeUrls = $this->storeFactory->getWriteBaseUrls();
+ } else {
+ $storeUrls = is_array( $storeUrls ) ? $storeUrls : [ $storeUrls ];
+ }
+
+ if ( !$storeUrls ) {
return false; // no stores exists which can be "read only"
}
- foreach ( $writableStores as $storeUrl ) {
+ foreach ( $storeUrls as $storeUrl ) {
$store = $this->storeFactory->getStoreForUrl( $storeUrl );
$location = $this->storeFactory->getStoreLocationFromUrl( $storeUrl );
if ( $store !== false && !$store->isReadOnly( $location ) ) {
$lb = $this->getLoadBalancer( $cluster );
$domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
- if ( !in_array( $cluster, $this->writableLocations, true ) ) {
- $this->logger->debug( "read only external store\n" );
- $lb->allowLagged( true );
- } else {
- $this->logger->debug( "writable external store\n" );
- }
-
$db = $lb->getConnectionRef( DB_REPLICA, [], $domainId );
$db->clearFlag( DBO_TRX ); // sanity
}
/**
- * @return string[] List of base URLs for writes, e.g. [ "DB://cluster1" ]
+ * @return string[] List of default base URLs for writes, e.g. [ "DB://cluster1" ]
* @since 1.34
*/
public function getWriteBaseUrls() {
$params['domain'] = $this->localDomainId; // default
$params['isDomainImplicit'] = true; // b/c for ExternalStoreDB
}
- $params['writableLocations'] = [];
- // Determine the locations for this protocol/store still receiving writes
- foreach ( $this->writeBaseUrls as $storeUrl ) {
- list( $storeProto, $storePath ) = self::splitStorageUrl( $storeUrl );
- if ( $protoLowercase === strtolower( $storeProto ) ) {
- $params['writableLocations'][] = $storePath;
- }
- }
// @TODO: ideally, this class should not hardcode what classes need what backend factory
// objects. For now, inject the factory instances into __construct() for those that do.
if ( $protoLowercase === 'db' ) {
protected $dbDomain;
/** @var bool Whether this was factoried with an explicit DB domain */
protected $isDbDomainExplicit;
- /** @var string[] Writable locations */
- protected $writableLocations = [];
/** @var LoggerInterface */
protected $logger;
/**
* @param array $params Usage context options for this instance:
* - domain: the DB domain ID of the wiki the content is for [required]
- * - writableLocations: locations that are writable [required]
* - logger: LoggerInterface instance [optional]
* - isDomainImplicit: whether this was factoried without an explicit DB domain [optional]
*/
}
$this->logger = $params['logger'] ?? new NullLogger();
- $this->writableLocations = $params['writableLocations'] ?? [];
}
public function setLogger( LoggerInterface $logger ) {
* @since 1.31
*/
public function isReadOnly( $location ) {
- return !in_array( $location, $this->writableLocations, true );
+ return false;
}
}
<?php
+use Wikimedia\Rdbms\LBFactory;
+
/**
* @covers ExternalStoreFactory
* @covers ExternalStoreAccess
* @covers ExternalStoreFactory::getStore
*/
public function testStoreFactoryBasic() {
- $active = [ 'memory' ];
- $defaults = [ 'memory://cluster1', 'memory://cluster2' ];
+ $active = [ 'memory', 'mwstore' ];
+ $defaults = [ 'memory://cluster1', 'memory://cluster2', 'mwstore://memstore1' ];
$esFactory = new ExternalStoreFactory( $active, $defaults, 'db-prefix' );
+ $this->setMwGlobals( 'wgFileBackends', [
+ [
+ 'name' => 'memstore1',
+ 'class' => 'MemoryFileBackend',
+ 'domain' => 'its-all-in-your-head',
+ 'readOnly' => 'reason is a lie',
+ 'lockManager' => 'nullLockManager'
+ ]
+ ] );
$this->assertEquals( $active, $esFactory->getProtocols() );
$this->assertEquals( $defaults, $esFactory->getWriteBaseUrls() );
/** @var ExternalStoreMemory $store */
$store = $esFactory->getStore( 'memory' );
$this->assertInstanceOf( ExternalStoreMemory::class, $store );
- $this->assertEquals( false, $store->isReadOnly( 'cluster1' ) );
- $this->assertEquals( false, $store->isReadOnly( 'cluster2' ) );
- $this->assertEquals( true, $store->isReadOnly( 'clusterOld' ) );
+ $this->assertFalse( $store->isReadOnly( 'cluster1' ), "Location is writable" );
+ $this->assertFalse( $store->isReadOnly( 'cluster2' ), "Location is writable" );
+
+ $mwStore = $esFactory->getStore( 'mwstore' );
+ $this->assertTrue( $mwStore->isReadOnly( 'memstore1' ), "Location is read-only" );
$lb = $this->getMockBuilder( \Wikimedia\Rdbms\LoadBalancer::class )
->disableOriginalConstructor()->getMock();
$lb->expects( $this->any() )->method( 'getReadOnlyReason' )->willReturn( 'Locked' );
- $lbFactory = $this->getMockBuilder( \Wikimedia\Rdbms\LBFactory::class )
+ $lbFactory = $this->getMockBuilder( LBFactory::class )
->disableOriginalConstructor()->getMock();
$lbFactory->expects( $this->any() )->method( 'getExternalLB' )->willReturn( $lb );