Make interwiki transclusion use the WAN cache
authorAaron Schulz <aschulz@wikimedia.org>
Fri, 8 Jun 2018 20:43:03 +0000 (13:43 -0700)
committerAaron Schulz <aschulz@wikimedia.org>
Mon, 27 Aug 2018 19:32:04 +0000 (19:32 +0000)
This means that now:
* Entries actually get deleted when expired
* The transclusion cache is shared across wikis
* Large blobs that do not fit in cache no longer cause DB errors
* DB writes are not triggered on GET requests
* Keys are hashed and no longer need to be so restrictive

Also, add a "check key" based purge system and process cache the
text/html values similar to how regular revision text is cached.

Bug: T189702
Change-Id: I8ac12b53c02bb26857175dd5a4af29d49e03dc33

includes/page/WikiPage.php
includes/parser/Parser.php

index a1b2e57..e1b37c0 100644 (file)
@@ -3152,6 +3152,9 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Image redirects
                RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
+
+               // Purge cross-wiki cache entities referencing this page
+               self::purgeInterwikiCheckKey( $title );
        }
 
        /**
@@ -3190,14 +3193,41 @@ class WikiPage implements Page, IDBAccessObject {
                // Clear file cache for this page only
                HTMLFileCache::clearFileCache( $title );
 
+               // Purge ?action=info cache
                $revid = $revision ? $revision->getId() : null;
                DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
                        InfoAction::invalidateCache( $title, $revid );
                } );
+
+               // Purge cross-wiki cache entities referencing this page
+               self::purgeInterwikiCheckKey( $title );
        }
 
        /**#@-*/
 
+       /**
+        * Purge the check key for cross-wiki cache entries referencing this page
+        *
+        * @param Title $title
+        */
+       private static function purgeInterwikiCheckKey( Title $title ) {
+               global $wgEnableScaryTranscluding;
+
+               if ( !$wgEnableScaryTranscluding ) {
+                       return; // @todo: perhaps this wiki is only used as a *source* for content?
+               }
+
+               DeferredUpdates::addCallableUpdate( function () use ( $title ) {
+                       $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+                       $cache->resetCheckKey(
+                               // Do not include the namespace since there can be multiple aliases to it
+                               // due to different namespace text definitions on different wikis. This only
+                               // means that some cache invalidations happen that are not strictly needed.
+                               $cache->makeGlobalKey( 'interwiki-page', wfWikiID(), $title->getDBkey() )
+                       );
+               } );
+       }
+
        /**
         * Returns a list of categories this page is a member of.
         * Results will include hidden categories
index 12d899b..3920e7b 100644 (file)
@@ -3739,57 +3739,68 @@ class Parser {
         * Transclude an interwiki link.
         *
         * @param Title $title
-        * @param string $action
+        * @param string $action Usually one of (raw, render)
         *
         * @return string
         */
        public function interwikiTransclude( $title, $action ) {
-               global $wgEnableScaryTranscluding;
+               global $wgEnableScaryTranscluding, $wgTranscludeCacheExpiry;
 
                if ( !$wgEnableScaryTranscluding ) {
                        return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
                }
 
                $url = $title->getFullURL( [ 'action' => $action ] );
-
-               if ( strlen( $url ) > 255 ) {
+               if ( strlen( $url ) > 1024 ) {
                        return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
                }
-               return $this->fetchScaryTemplateMaybeFromCache( $url );
-       }
 
-       /**
-        * @param string $url
-        * @return mixed|string
-        */
-       public function fetchScaryTemplateMaybeFromCache( $url ) {
-               global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB( DB_REPLICA );
-               $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
-               $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
-                               [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
-               if ( $obj ) {
-                       return $obj->tc_contents;
-               }
-
-               $req = MWHttpRequest::factory( $url, [], __METHOD__ );
-               $status = $req->execute(); // Status object
-               if ( $status->isOK() ) {
-                       $text = $req->getContent();
-               } elseif ( $req->getStatus() != 200 ) {
+               $wikiId = $title->getTransWikiID(); // remote wiki ID or false
+
+               $fname = __METHOD__;
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+
+               $data = $cache->getWithSetCallback(
+                       $cache->makeGlobalKey(
+                               'interwiki-transclude',
+                               ( $wikiId !== false ) ? $wikiId : 'external',
+                               sha1( $url )
+                       ),
+                       $wgTranscludeCacheExpiry,
+                       function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
+                               $req = MWHttpRequest::factory( $url, [], $fname );
+
+                               $status = $req->execute(); // Status object
+                               if ( !$status->isOK() ) {
+                                       $ttl = $cache::TTL_UNCACHEABLE;
+                               } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
+                                       $ttl = min( $cache::TTL_LAGGED, $ttl );
+                               }
+
+                               return [
+                                       'text' => $status->isOK() ? $req->getContent() : null,
+                                       'code' => $req->getStatus()
+                               ];
+                       },
+                       [
+                               'checkKeys' => ( $wikiId !== false )
+                                       ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
+                                       : [],
+                               'pcGroup' => 'interwiki-transclude:5',
+                               'pcTTL' => $cache::TTL_PROC_LONG
+                       ]
+               );
+
+               if ( is_string( $data['text'] ) ) {
+                       $text = $data['text'];
+               } elseif ( $data['code'] != 200 ) {
                        // Though we failed to fetch the content, this status is useless.
-                       return wfMessage( 'scarytranscludefailed-httpstatus' )
-                               ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+                       $text = wfMessage( 'scarytranscludefailed-httpstatus' )
+                               ->params( $url, $data['code'] )->inContentLanguage()->text();
                } else {
-                       return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
+                       $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
                }
 
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->replace( 'transcache', [ 'tc_url' ], [
-                       'tc_url' => $url,
-                       'tc_time' => $dbw->timestamp( time() ),
-                       'tc_contents' => $text
-               ] );
                return $text;
        }