From 15cb57b3b5c5b665f4602baccfc320701b3ef6aa Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Tue, 22 Aug 2017 14:36:15 -0700 Subject: [PATCH] Avoid preemptive DB replication waits for farm cross-wiki redirects This previously only worked if $wgLocalVirtualHosts was set, which was too specific to check and not used by WMF. Use the more generic WikiMap class. Two methods have been added there to do the work of enumerating canonical wiki farm URLs and checking them against a given URL. Bug: T172357 Change-Id: Id2415bab5d7f5a08b9f536858c32d329138384a2 --- includes/DefaultSettings.php | 6 +-- includes/MediaWiki.php | 34 +++---------- includes/WikiMap.php | 69 +++++++++++++++++++++++++- tests/phpunit/includes/WikiMapTest.php | 58 ++++++++++++++++++++++ 4 files changed, 134 insertions(+), 33 deletions(-) diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 26138892b5..d525816bf9 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -2053,8 +2053,8 @@ $wgDBmysql5 = false; $wgDBOracleDRCP = false; /** - * Other wikis on this site, can be administered from a single developer - * account. + * Other wikis on this site, can be administered from a single developer account. + * * Array numeric key => database name */ $wgLocalDatabases = []; @@ -8308,8 +8308,6 @@ $wgHTTPProxy = false; * subdomain thereof, then no proxy will be used. * Command-line scripts are not affected by this setting and will always use * the proxy if it is configured. - * - ChronologyProtector: Decide to shutdown LBFactory asynchronously instead - * synchronously if the current response redirects to a local virtual host. * * @since 1.25 */ diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 10b9e2b9ca..7b59ee93dc 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -607,7 +607,7 @@ class MediaWiki { $request->wasPosted() && $output->getRedirect() && $lbFactory->hasOrMadeRecentMasterChanges( INF ) - ) ? self::getUrlDomainDistance( $output->getRedirect(), $context ) : false; + ) ? self::getUrlDomainDistance( $output->getRedirect() ) : false; $allowHeaders = !( $output->isDisabled() || headers_sent() ); if ( $urlDomainDistance === 'local' || $urlDomainDistance === 'remote' ) { @@ -676,34 +676,14 @@ class MediaWiki { /** * @param string $url - * @param IContextSource $context * @return string Either "local", "remote" if in the farm, "external" otherwise */ - private static function getUrlDomainDistance( $url, IContextSource $context ) { - static $relevantKeys = [ 'host' => true, 'port' => true ]; - - $infoCandidate = wfParseUrl( $url ); - if ( $infoCandidate === false ) { - return 'external'; - } - - $infoCandidate = array_intersect_key( $infoCandidate, $relevantKeys ); - $clusterHosts = array_merge( - // Local wiki host (the most common case) - [ $context->getConfig()->get( 'CanonicalServer' ) ], - // Any local/remote wiki virtual hosts for this wiki farm - $context->getConfig()->get( 'LocalVirtualHosts' ) - ); - - foreach ( $clusterHosts as $i => $clusterHost ) { - $parseUrl = wfParseUrl( $clusterHost ); - if ( !$parseUrl ) { - continue; - } - $infoHost = array_intersect_key( $parseUrl, $relevantKeys ); - if ( $infoCandidate === $infoHost ) { - return ( $i === 0 ) ? 'local' : 'remote'; - } + private static function getUrlDomainDistance( $url ) { + $clusterWiki = WikiMap::getWikiFromUrl( $url ); + if ( $clusterWiki === wfWikiID() ) { + return 'local'; // the current wiki + } elseif ( $clusterWiki !== false ) { + return 'remote'; // another wiki in this cluster/farm } return 'external'; diff --git a/includes/WikiMap.php b/includes/WikiMap.php index 6a532e5da5..4f3c461699 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -20,8 +20,10 @@ * @file */ +use MediaWiki\MediaWikiServices; + /** - * Helper tools for dealing with other wikis. + * Helper tools for dealing with other locally-hosted wikis. */ class WikiMap { @@ -81,7 +83,7 @@ class WikiMap { * @return WikiReference|null WikiReference object or null if the wiki was not found */ private static function getWikiWikiReferenceFromSites( $wikiID ) { - $siteLookup = \MediaWiki\MediaWikiServices::getInstance()->getSiteLookup(); + $siteLookup = MediaWikiServices::getInstance()->getSiteLookup(); $site = $siteLookup->getSite( $wikiID ); if ( !$site instanceof MediaWikiSite ) { @@ -174,4 +176,67 @@ class WikiMap { return false; } + + /** + * Get canonical server info for all local wikis in the map that have one + * + * @return array Map of (local wiki ID => map of (url,parts)) + * @since 1.30 + */ + public static function getCanonicalServerInfoForAllWikis() { + $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); + + return $cache->getWithSetCallback( + $cache->makeGlobalKey( 'wikimap', 'canonical-urls' ), + $cache::TTL_DAY, + function () { + global $wgLocalDatabases, $wgCanonicalServer; + + $infoMap = []; + // Make sure at least the current wiki is set, for simple configurations. + // This also makes it the first in the map, which is useful for common cases. + $infoMap[wfWikiID()] = [ + 'url' => $wgCanonicalServer, + 'parts' => wfParseUrl( $wgCanonicalServer ) + ]; + + foreach ( $wgLocalDatabases as $wikiId ) { + $wikiReference = self::getWiki( $wikiId ); + if ( $wikiReference ) { + $url = $wikiReference->getCanonicalServer(); + $infoMap[$wikiId] = [ 'url' => $url, 'parts' => wfParseUrl( $url ) ]; + } + } + + return $infoMap; + } + ); + } + + /** + * @param string $url + * @return bool|string Wiki ID or false + * @since 1.30 + */ + public static function getWikiFromUrl( $url ) { + $urlPartsCheck = wfParseUrl( $url ); + if ( $urlPartsCheck === false ) { + return false; + } + + $urlPartsCheck = array_intersect_key( $urlPartsCheck, [ 'host' => 1, 'port' => 1 ] ); + foreach ( self::getCanonicalServerInfoForAllWikis() as $wikiId => $info ) { + $urlParts = $info['parts']; + if ( $urlParts === false ) { + continue; // sanity + } + + $urlParts = array_intersect_key( $urlParts, [ 'host' => 1, 'port' => 1 ] ); + if ( $urlParts == $urlPartsCheck ) { + return $wikiId; + } + } + + return false; + } } diff --git a/tests/phpunit/includes/WikiMapTest.php b/tests/phpunit/includes/WikiMapTest.php index 12878b37ed..186ffdbcc3 100644 --- a/tests/phpunit/includes/WikiMapTest.php +++ b/tests/phpunit/includes/WikiMapTest.php @@ -16,6 +16,7 @@ class WikiMapTest extends MediaWikiLangTestCase { 'enwiki' => 'http://en.example.org', 'ruwiki' => '//ru.example.org', 'nopathwiki' => '//nopath.example.org', + 'thiswiki' => '//this.wiki.org' ], 'wgArticlePath' => [ 'enwiki' => '/w/$1', @@ -25,6 +26,10 @@ class WikiMapTest extends MediaWikiLangTestCase { $conf->suffixes = [ 'wiki' ]; $this->setMwGlobals( [ 'wgConf' => $conf, + 'wgLocalDatabases' => [ 'enwiki', 'ruwiki', 'nopathwiki' ], + 'wgCanonicalServer' => '//this.wiki.org', + 'wgDBname' => 'thiswiki', + 'wgDBprefix' => '' ] ); TestSites::insertIntoDb(); @@ -175,4 +180,57 @@ class WikiMapTest extends MediaWikiLangTestCase { $this->assertEquals( $expected, WikiMap::getForeignURL( $wikiId, $page, $fragment ) ); } + /** + * @covers WikiMap::getCanonicalServerInfoForAllWikis() + */ + public function testGetCanonicalServerInfoForAllWikis() { + $expected = [ + 'thiswiki' => [ + 'url' => '//this.wiki.org', + 'parts' => [ 'scheme' => '', 'host' => 'this.wiki.org', 'delimiter' => '//' ] + ], + 'enwiki' => [ + 'url' => 'http://en.example.org', + 'parts' => [ + 'scheme' => 'http', 'host' => 'en.example.org', 'delimiter' => '://' ] + ], + 'ruwiki' => [ + 'url' => '//ru.example.org', + 'parts' => [ 'scheme' => '', 'host' => 'ru.example.org', 'delimiter' => '//' ] + ] + ]; + + $this->assertArrayEquals( + $expected, + WikiMap::getCanonicalServerInfoForAllWikis(), + true, + true + ); + } + + public function provideGetWikiFromUrl() { + return [ + [ 'http://this.wiki.org', 'thiswiki' ], + [ 'https://this.wiki.org', 'thiswiki' ], + [ 'http://this.wiki.org/$1', 'thiswiki' ], + [ 'https://this.wiki.org/$2', 'thiswiki' ], + [ 'http://en.example.org', 'enwiki' ], + [ 'https://en.example.org', 'enwiki' ], + [ 'http://en.example.org/$1', 'enwiki' ], + [ 'https://en.example.org/$2', 'enwiki' ], + [ 'http://ru.example.org', 'ruwiki' ], + [ 'https://ru.example.org', 'ruwiki' ], + [ 'http://ru.example.org/$1', 'ruwiki' ], + [ 'https://ru.example.org/$2', 'ruwiki' ], + [ 'http://not.defined.org', false ] + ]; + } + + /** + * @dataProvider provideGetWikiFromUrl + * @covers WikiMap::getWikiFromUrl() + */ + public function testGetWikiFromUrl( $url, $wiki ) { + $this->assertEquals( $wiki, WikiMap::getWikiFromUrl( $url ) ); + } } -- 2.20.1