From 9e49260fc9586c962f8c4da64ff4c0f355c19a07 Mon Sep 17 00:00:00 2001 From: daniel Date: Wed, 2 Jun 2021 17:49:19 +0200 Subject: [PATCH] Make LinkCache behavior more consistent This patch does several things to LinkCache to make its behavior more consistent and predictable: * Methods that set a "good" link now clear the "bad link" flag, and vice versa. * invalidateTitle() now also clears the local cache, not just the persistent cache. * Attempts to set data for LinkTargets that are not proper local pages are ignored. * All methods now accept LinkTarget|PageRecord as the key. The ones that previously accepted a string still allow that as well. * addLinkObject() now consistently uses the local cache if possible, and consistently bypasses it if the forUpdate() flag is set. This is all done in preparation for LinkCache being used inside PageStore. Bug: T278940 Change-Id: I62107789fa185606a81be20ffa8f0be48297c08f --- includes/ServiceWiring.php | 4 +- includes/cache/LinkCache.php | 281 ++++++++++---- .../phpunit/includes/cache/LinkCacheTest.php | 347 ++++++++++++++++++ 3 files changed, 566 insertions(+), 66 deletions(-) create mode 100644 tests/phpunit/includes/cache/LinkCacheTest.php diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index a5c0d6b1f57..2f76ebd63ba 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -674,12 +674,14 @@ return [ $dbLoadBalancer = $services->isServiceDisabled( 'DBLoadBalancer' ) ? null : $services->getDBLoadBalancer(); - return new LinkCache( + $linkCache = new LinkCache( $services->getTitleFormatter(), $services->getMainWANObjectCache(), $services->getNamespaceInfo(), $dbLoadBalancer ); + $linkCache->setLogger( LoggerFactory::getInstance( 'LinkCache' ) ); + return $linkCache; }, 'LinkRenderer' => static function ( MediaWikiServices $services ) : LinkRenderer { diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index a3b1b2a841c..7c766902f6c 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -23,6 +23,11 @@ use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\PageIdentity; +use MediaWiki\Page\PageReference; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; use Wikimedia\Rdbms\Database; use Wikimedia\Rdbms\IDatabase; use Wikimedia\Rdbms\ILoadBalancer; @@ -32,7 +37,7 @@ use Wikimedia\Rdbms\ILoadBalancer; * * @ingroup Cache */ -class LinkCache { +class LinkCache implements LoggerAwareInterface { /** @var MapCacheLRU */ private $goodLinks; /** @var MapCacheLRU */ @@ -52,6 +57,9 @@ class LinkCache { /** @var ILoadBalancer|null */ private $loadBalancer; + /** @var LoggerInterface */ + private $logger; + /** * How many Titles to store. There are two caches, so the amount actually * stored in memory can be up to twice this. @@ -80,6 +88,14 @@ class LinkCache { $this->titleFormatter = $titleFormatter; $this->nsInfo = $nsInfo; $this->loadBalancer = $loadBalancer; + $this->logger = new NullLogger(); + } + + /** + * @param LoggerInterface $logger + */ + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; } /** @@ -110,11 +126,71 @@ class LinkCache { } /** - * @param string $title Prefixed DB key - * @return int Page ID or zero + * @param LinkTarget|PageReference|string $page + * @param bool $passThrough Return $page if $page is a string + * + * @return ?string the cache key */ - public function getGoodLinkID( $title ) { - $info = $this->goodLinks->get( $title ); + private function getCacheKey( $page, $passThrough = false ) { + if ( is_string( $page ) ) { + if ( $passThrough ) { + return $page; + } else { + throw new InvalidArgumentException( 'They key may not be given as a string here' ); + } + } + + if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) { + // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future. + $this->logger->info( + 'cross-wiki page reference', + [ 'page-wiki' => $page->getWikiId(), 'page-reference' => $page ] + ); + return null; + } + + if ( $page instanceof PageIdentity && !$page->canExist() ) { + // Non-proper page, perhaps a special page or interwiki link or relative section link. + $this->logger->warning( + 'non-proper page reference', + [ 'page-reference' => $page ] + ); + return null; + } + + if ( $page instanceof LinkTarget + && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 ) + ) { + // Interwiki link or relative section link. These do not have a page ID, so they + // can neither be "good" nor "bad" in the sense of this class. + $this->logger->warning( + 'link to non-proper page', + [ 'page-link' => $page ] + ); + return null; + } + + return $this->titleFormatter->getPrefixedDBkey( $page ); + } + + /** + * Returns the ID of the given page, if information about this page has been cached. + * + * @param LinkTarget|PageReference|string $page The page to get the ID for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. + * @return int Page ID, or zero if the page was not cached or does not exist or is not a + * proper page (e.g. a special page or an interwiki link). + */ + public function getGoodLinkID( $page ) { + $key = $this->getCacheKey( $page, true ); + + if ( $key === null ) { + return 0; + } + + $info = $this->goodLinks->get( $key ); + if ( !$info ) { return 0; } @@ -122,15 +198,22 @@ class LinkCache { } /** - * Get a field of a title object from cache. + * Get a field of a page from the cache. + * * If this link is not a cached good title, it will return NULL. - * @param LinkTarget $target - * @param string $field ('length','redirect','revision','model') - * @return string|int|null + * @param LinkTarget|PageReference $page The page to get cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. + * @param string $field ( 'id', 'length', 'redirect', 'revision', 'model', 'lang', 'restrictions' ) + * @return string|int|null The field value, or null if the page was not cached or does not exist + * or is not a proper page (e.g. a special page or interwiki link). */ - public function getGoodLinkFieldObj( LinkTarget $target, $field ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $info = $this->goodLinks->get( $dbkey ); + public function getGoodLinkFieldObj( $page, string $field ) { + $key = $this->getCacheKey( $page ); + if ( $key === null ) { + return null; + } + + $info = $this->goodLinks->get( $key ); if ( !$info ) { return null; } @@ -138,30 +221,43 @@ class LinkCache { } /** - * @param string $title Prefixed DB key - * @return bool + * Returns true if the fact that this page does not exist had been added to the cache. + * + * @param LinkTarget|PageReference|string $page The page to get cached info for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. + * @return bool True if the page is known to not exist. */ - public function isBadLink( $title ) { - // Use get() to ensure it records as used for LRU. - return $this->badLinks->has( $title ); + public function isBadLink( $page ) { + $key = $this->getCacheKey( $page, true ); + + return $key !== null && $this->badLinks->has( $key ); } /** - * Add a link for the title to the link cache + * Add information about an existing page to the cache. + * + * @see addGoodLinkObjFromRow() * * @param int $id Page's ID - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @param int $len Text's length * @param int|null $redir Whether the page is a redirect * @param int $revision Latest revision's ID * @param string|null $model Latest revision's content model ID * @param string|null $lang Language code of the page, if not the content language */ - public function addGoodLinkObj( $id, LinkTarget $target, $len = -1, $redir = null, + public function addGoodLinkObj( $id, $page, $len = -1, $redir = null, $revision = 0, $model = null, $lang = null ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->goodLinks->set( $dbkey, [ + $key = $this->getCacheKey( $page ); + + if ( $key === null ) { + return; + } + + $this->goodLinks->set( $key, [ 'id' => (int)$id, 'length' => (int)$len, 'redirect' => (int)$redir, @@ -170,18 +266,26 @@ class LinkCache { 'lang' => $lang ? (string)$lang : null, 'restrictions' => null ] ); + $this->badLinks->clear( $key ); } /** * Same as above with better interface. * @since 1.19 - * @param LinkTarget $target - * @param stdClass $row Object which has the fields page_id, page_is_redirect, - * page_latest and page_content_model + * + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. + * @param stdClass $row Object which has all fields returned by getSelectFields(). + * */ - public function addGoodLinkObjFromRow( LinkTarget $target, $row ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->goodLinks->set( $dbkey, [ + public function addGoodLinkObjFromRow( $page, stdClass $row ) { + $key = $this->getCacheKey( $page ); + + if ( $key === null ) { + return; + } + + $this->goodLinks->set( $key, [ 'id' => intval( $row->page_id ), 'length' => intval( $row->page_len ), 'redirect' => intval( $row->page_is_redirect ), @@ -196,32 +300,45 @@ class LinkCache { ? strval( $row->page_restrictions ) : null ] ); + $this->badLinks->clear( $key ); } /** - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to set cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. */ - public function addBadLinkObj( LinkTarget $target ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - if ( !$this->isBadLink( $dbkey ) ) { - $this->badLinks->set( $dbkey, 1 ); + public function addBadLinkObj( $page ) { + $key = $this->getCacheKey( $page ); + if ( $key !== null && !$this->isBadLink( $key ) ) { + $this->badLinks->set( $key, 1 ); + $this->goodLinks->clear( $key ); } } /** - * @param string $title Prefixed DB key + * @param LinkTarget|PageReference|string $page The page to clear cached info for, + * as an object or a prefixed DB key. + * In MediaWiki 1.36 and earlier, only a string was accepted. */ - public function clearBadLink( $title ) { - $this->badLinks->clear( $title ); + public function clearBadLink( $page ) { + $key = $this->getCacheKey( $page, true ); + + if ( $key !== null ) { + $this->badLinks->clear( $key ); + } } /** - * @param LinkTarget $target + * @param LinkTarget|PageReference $page The page to clear cached info for. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. */ - public function clearLink( LinkTarget $target ) { - $dbkey = $this->titleFormatter->getPrefixedDBkey( $target ); - $this->badLinks->clear( $dbkey ); - $this->goodLinks->clear( $dbkey ); + public function clearLink( $page ) { + $key = $this->getCacheKey( $page ); + + if ( $key !== null ) { + $this->badLinks->clear( $key ); + $this->goodLinks->clear( $key ); + } } /** @@ -250,25 +367,37 @@ class LinkCache { } /** - * Add a title to the link cache, return the page_id or zero if non-existent + * Add a title to the link cache, return the page_id or zero if non-existent. + * This causes the link to be looked up in the database if it is not yet cached. + * + * @param LinkTarget|PageReference $page The page to load. + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * - * @param LinkTarget $nt LinkTarget object to add * @return int Page ID or zero */ - public function addLinkObj( LinkTarget $nt ) { - $key = $this->titleFormatter->getPrefixedDBkey( $nt ); - if ( $this->isBadLink( $key ) || $nt->isExternal() || $nt->getNamespace() < 0 ) { - return 0; - } - $id = $this->getGoodLinkID( $key ); - if ( $id != 0 ) { - return $id; + public function addLinkObj( $page ) { + if ( $page instanceof LinkTarget ) { + $nt = $page; + } else { + $nt = TitleValue::castPageToLinkTarget( $page ); } - if ( $key === '' ) { + $key = $this->getCacheKey( $nt ); + if ( $key === null ) { return 0; } + if ( !$this->mForUpdate ) { + $id = $this->getGoodLinkID( $key ); + if ( $id != 0 ) { + return $id; + } + + if ( $this->isBadLink( $key ) ) { + return 0; + } + } + // Only query database, when load balancer is provided by service wiring // This maybe not happen when running as part of the installer if ( $this->loadBalancer === null ) { @@ -312,20 +441,32 @@ class LinkCache { /** * @param WANObjectCache $cache - * @param LinkTarget $t + * @param LinkTarget|Pagereference $page + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @return string[] * @since 1.28 */ - public function getMutableCacheKeys( WANObjectCache $cache, LinkTarget $t ) { - if ( $this->isCacheable( $t ) ) { - return [ $cache->makeKey( 'page', $t->getNamespace(), sha1( $t->getDBkey() ) ) ]; + public function getMutableCacheKeys( WANObjectCache $cache, $page ) { + $key = $this->getCacheKey( $page ); + // if no key can be derived, the page isn't cacheable + if ( $key === null ) { + return []; + } + + if ( $this->isCacheable( $page ) ) { + return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ]; } return []; } - private function isCacheable( LinkTarget $title ) { - $ns = $title->getNamespace(); + /** + * @param LinkTarget|PageReference $page + * + * @return bool + */ + private function isCacheable( $page ) { + $ns = $page->getNamespace(); if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) { return true; } @@ -337,16 +478,22 @@ class LinkCache { return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) ); } - private function fetchPageRow( IDatabase $db, LinkTarget $nt ) { + /** + * @param IDatabase $db + * @param LinkTarget|PageReference $page + * + * @return stdClass|false + */ + private function fetchPageRow( IDatabase $db, $page ) { $fields = self::getSelectFields(); - if ( $this->isCacheable( $nt ) ) { + if ( $this->isCacheable( $page ) ) { $fields[] = 'page_touched'; } return $db->selectRow( 'page', $fields, - [ 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ], + [ 'page_namespace' => $page->getNamespace(), 'page_title' => $page->getDBkey() ], __METHOD__ ); } @@ -354,16 +501,19 @@ class LinkCache { /** * Purge the link cache for a title * - * @param LinkTarget $title + * @param LinkTarget|PageReference $page + * In MediaWiki 1.36 and earlier, only LinkTarget was accepted. * @since 1.28 */ - public function invalidateTitle( LinkTarget $title ) { - if ( $this->isCacheable( $title ) ) { + public function invalidateTitle( $page ) { + if ( $this->isCacheable( $page ) ) { $cache = $this->wanCache; $cache->delete( - $cache->makeKey( 'page', $title->getNamespace(), sha1( $title->getDBkey() ) ) + $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ); } + + $this->clearLink( $page ); } /** @@ -373,4 +523,5 @@ class LinkCache { $this->goodLinks->clear(); $this->badLinks->clear(); } + } diff --git a/tests/phpunit/includes/cache/LinkCacheTest.php b/tests/phpunit/includes/cache/LinkCacheTest.php new file mode 100644 index 00000000000..cb34d8fdf4e --- /dev/null +++ b/tests/phpunit/includes/cache/LinkCacheTest.php @@ -0,0 +1,347 @@ + new EmptyBagOStuff() ] ); + } + + return new LinkCache( + $this->getServiceContainer()->getTitleFormatter(), + $wanCache, + $this->getServiceContainer()->getNamespaceInfo(), + $this->getServiceContainer()->getDBLoadBalancer() + ); + } + + public function providePageAndLink() { + return [ + [ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ], + [ new TitleValue( NS_USER, __METHOD__ ) ], + ]; + } + + public function providePageAndLinkAndString() { + return [ + [ new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ) ], + [ new TitleValue( NS_USER, __METHOD__ ) ], + [ 'User:' . __METHOD__ ], + ]; + } + + private function getPageRow() { + return (object)[ + 'page_id' => 8, + 'page_len' => 18, + 'page_is_redirect' => 0, + 'page_latest' => 118, + 'page_content_model' => CONTENT_MODEL_TEXT, + 'page_lang' => 'xyz', + 'page_restrictions' => 'test' + ]; + } + + /** + * @dataProvider providePageAndLink + * @covers LinkCache::addGoodLinkObjFromRow() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + * @covers LinkCache::clearLink() + */ + public function testAddGoodLinkObjFromRow( $page ) { + $linkCache = $this->newLinkCache(); + + $row = $this->getPageRow(); + + $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); + $linkCache->addBadLinkObj( $page ); + $linkCache->addGoodLinkObjFromRow( $page, $row ); + + $this->assertSame( $row->page_id, $linkCache->getGoodLinkID( $page ) ); + $this->assertFalse( $linkCache->isBadLink( $page ) ); + + $this->assertSame( + $row->page_id, + $linkCache->getGoodLinkFieldObj( $page, 'id' ) + ); + $this->assertSame( + $row->page_len, + $linkCache->getGoodLinkFieldObj( $page, 'length' ) + ); + $this->assertSame( + $row->page_is_redirect, + $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) + ); + $this->assertSame( + $row->page_latest, + $linkCache->getGoodLinkFieldObj( $page, 'revision' ) + ); + $this->assertSame( + $row->page_content_model, + $linkCache->getGoodLinkFieldObj( $page, 'model' ) + ); + $this->assertSame( + $row->page_lang, + $linkCache->getGoodLinkFieldObj( $page, 'lang' ) + ); + $this->assertSame( + $row->page_restrictions, + $linkCache->getGoodLinkFieldObj( $page, 'restrictions' ) + ); + + $linkCache->clearBadLink( $page ); + $this->assertNotNull( $linkCache->getGoodLinkID( $page ) ); + $this->assertNotNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) ); + + $linkCache->clearLink( $page ); + $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) ); + $this->assertNull( $linkCache->getGoodLinkFieldObj( $page, 'length' ) ); + } + + /** + * @dataProvider providePageAndLink + * @covers LinkCache::addGoodLinkObj() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + */ + public function testAddGoodLinkObjWithAllParameters( $page ) { + $linkCache = $this->newLinkCache(); + + $linkCache->addGoodLinkObj( + 8, + $page, + 18, + 0, + 118, + CONTENT_MODEL_TEXT, + 'xyz' + ); + + $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) ); + $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) ); + + $this->assertSame( + 18, + $linkCache->getGoodLinkFieldObj( $page, 'length' ) + ); + $this->assertSame( + 0, + $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) + ); + $this->assertSame( + 118, + $linkCache->getGoodLinkFieldObj( $page, 'revision' ) + ); + $this->assertSame( + CONTENT_MODEL_TEXT, + $linkCache->getGoodLinkFieldObj( $page, 'model' ) + ); + $this->assertSame( + 'xyz', + $linkCache->getGoodLinkFieldObj( $page, 'lang' ) + ); + } + + /** + * @covers LinkCache::addGoodLinkObj() + * @covers LinkCache::getGoodLinkID() + * @covers LinkCache::getGoodLinkFieldObj() + */ + public function testAddGoodLinkObjWithMinimalParameters() { + $linkCache = $this->newLinkCache(); + + $page = new PageReferenceValue( NS_USER, __METHOD__, PageReference::LOCAL ); + $linkCache->addGoodLinkObj( + 8, + $page + ); + + $this->assertSame( 8, $linkCache->getGoodLinkID( $page ) ); + $this->assertSame( 8, $linkCache->getGoodLinkFieldObj( $page, 'id' ) ); + + $this->assertSame( + -1, + $linkCache->getGoodLinkFieldObj( $page, 'length' ) + ); + $this->assertSame( + 0, + $linkCache->getGoodLinkFieldObj( $page, 'redirect' ) + ); + $this->assertSame( + 0, + $linkCache->getGoodLinkFieldObj( $page, 'revision' ) + ); + $this->assertSame( + null, + $linkCache->getGoodLinkFieldObj( $page, 'model' ) + ); + $this->assertSame( + null, + $linkCache->getGoodLinkFieldObj( $page, 'lang' ) + ); + } + + /** + * @covers LinkCache::addGoodLinkObj() + */ + public function testAddGoodLinkObjWithInterwikiLink() { + $linkCache = $this->newLinkCache(); + + $page = new TitleValue( NS_USER, __METHOD__, '', 'acme' ); + $linkCache->addGoodLinkObj( 8, $page ); + + $this->assertSame( 0, $linkCache->getGoodLinkID( $page ) ); + } + + /** + * @dataProvider providePageAndLink + * @covers LinkCache::addBadLinkObj() + * @covers LinkCache::isBadLink() + * @covers LinkCache::clearLink() + */ + public function testAddBadLinkObj( $key ) { + $linkCache = $this->newLinkCache(); + $this->assertFalse( $linkCache->isBadLink( $key ) ); + + $linkCache->addGoodLinkObj( 17, $key ); + + $linkCache->addBadLinkObj( $key ); + $this->assertTrue( $linkCache->isBadLink( $key ) ); + $this->assertSame( 0, $linkCache->getGoodLinkID( $key ) ); + + $linkCache->clearLink( $key ); + $this->assertFalse( $linkCache->isBadLink( $key ) ); + } + + /** + * @covers LinkCache::addBadLinkObj() + */ + public function testAddBadLinkObjWithInterwikiLink() { + $linkCache = $this->newLinkCache(); + + $page = new TitleValue( NS_USER, __METHOD__, '', 'acme' ); + $linkCache->addBadLinkObj( $page ); + + $this->assertFalse( $linkCache->isBadLink( $page ) ); + } + + /** + * @covers LinkCache::addLinkObj() + * @covers LinkCache::getGoodLinkFieldObj + */ + public function testAddLinkObj() { + $existing = $this->getExistingTestPage(); + $missing = $this->getNonexistingTestPage(); + + $linkCache = $this->newLinkCache(); + + $linkCache->addLinkObj( $existing ); + $linkCache->addLinkObj( $missing ); + + $this->assertTrue( $linkCache->isBadLink( $missing ) ); + $this->assertFalse( $linkCache->isBadLink( $existing ) ); + + $this->assertSame( $existing->getId(), $linkCache->getGoodLinkID( $existing ) ); + + // Make sure nothing explodes when getting a field from a non-existing entry + $this->assertNull( $linkCache->getGoodLinkFieldObj( $missing, 'length' ) ); + } + + /** + * @covers LinkCache::addLinkObj() + */ + public function testAddLinkObjUsesCachedInfo() { + $existing = $this->getExistingTestPage(); + $missing = $this->getNonexistingTestPage(); + + $row = $this->getPageRow(); + + $linkCache = $this->newLinkCache(); + + // pretend the existing page is missing, and the missing page exists + $linkCache->addGoodLinkObjFromRow( $missing, $row ); + $linkCache->addBadLinkObj( $existing ); + + // the LinkCache should use the cached info and not look into the database + $this->assertSame( (int)$row->page_id, $linkCache->addLinkObj( $missing ) ); + $this->assertSame( 0, $linkCache->addLinkObj( $existing ) ); + + // now set the "for update" flag and try again + $linkCache->forUpdate( true ); + $this->assertSame( 0, $linkCache->addLinkObj( $missing ) ); + $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing ) ); + } + + /** + * @covers LinkCache::addLinkObj() + * @covers LinkCache::getMutableCacheKeys() + */ + public function testAddLinkObjUsesWANCache() { + // Pages in some namespaces use the WAN cache: Template, File, Category, MediaWiki + $existing = $this->getExistingTestPage( Title::makeTitle( NS_TEMPLATE, __METHOD__ ) ); + + $fakeRow = $this->getPageRow(); + + $cache = new HashBagOStuff(); + $wanCache = new WANObjectCache( [ 'cache' => $cache ] ); + $linkCache = $this->newLinkCache( $wanCache ); + + // load the page row into the cache + $linkCache->addLinkObj( $existing ); + + $keys = $linkCache->getMutableCacheKeys( $wanCache, $existing ); + $this->assertNotEmpty( $keys ); + + foreach ( $keys as $key ) { + $this->assertNotFalse( $wanCache->get( $key ) ); + } + + // replace real row data with fake, and assert that it gets used + $wanCache->set( $key, $fakeRow ); + $linkCache->clearLink( $existing ); // clear local cache + $this->assertSame( (int)$fakeRow->page_id, $linkCache->addLinkObj( $existing ) ); + + // set the "for update" flag and try again + $linkCache->forUpdate( true ); + $this->assertSame( $existing->getId(), $linkCache->addLinkObj( $existing ) ); + } + + public function testFalsyPageName() { + $linkCache = $this->newLinkCache(); + + // The stringified value is "0", which is falsy in PHP! + $link = new TitleValue( NS_MAIN, '0' ); + + $linkCache->addBadLinkObj( $link ); + $this->assertTrue( $linkCache->isBadLink( $link ) ); + + $row = $this->getPageRow(); + $linkCache->addGoodLinkObjFromRow( $link, $row ); + $this->assertGreaterThan( 0, $linkCache->getGoodLinkID( $link ) ); + } + + public function testClearBadLinkWithString() { + $linkCache = $this->newLinkCache(); + $linkCache->clearBadLink( 'Xyzzy' ); + $this->addToAssertionCount( 1 ); + } + + public function testIsBadLinkWithString() { + $linkCache = $this->newLinkCache(); + $this->assertFalse( $linkCache->isBadLink( 'Xyzzy' ) ); + } + + public function testGetGoodLinkIdWithString() { + $linkCache = $this->newLinkCache(); + $this->assertSame( 0, $linkCache->getGoodLinkID( 'Xyzzy' ) ); + } +}