ParserCache.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <?php
  2. /**
  3. * Cache for outputs of the PHP parser
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Cache Parser
  22. */
  23. use MediaWiki\MediaWikiServices;
  24. /**
  25. * @ingroup Cache Parser
  26. * @todo document
  27. */
  28. class ParserCache {
  29. /**
  30. * Constants for self::getKey()
  31. * @since 1.30
  32. */
  33. /** Use only current data */
  34. const USE_CURRENT_ONLY = 0;
  35. /** Use expired data if current data is unavailable */
  36. const USE_EXPIRED = 1;
  37. /** Use expired data or data from different revisions if current data is unavailable */
  38. const USE_OUTDATED = 2;
  39. /**
  40. * Use expired data and data from different revisions, and if all else
  41. * fails vary on all variable options
  42. */
  43. const USE_ANYTHING = 3;
  44. /** @var BagOStuff */
  45. private $mMemc;
  46. /**
  47. * Anything cached prior to this is invalidated
  48. *
  49. * @var string
  50. */
  51. private $cacheEpoch;
  52. /**
  53. * Get an instance of this object
  54. *
  55. * @deprecated since 1.30, use MediaWikiServices instead
  56. * @return ParserCache
  57. */
  58. public static function singleton() {
  59. return MediaWikiServices::getInstance()->getParserCache();
  60. }
  61. /**
  62. * Setup a cache pathway with a given back-end storage mechanism.
  63. *
  64. * This class use an invalidation strategy that is compatible with
  65. * MultiWriteBagOStuff in async replication mode.
  66. *
  67. * @param BagOStuff $cache
  68. * @param string $cacheEpoch Anything before this timestamp is invalidated
  69. * @throws MWException
  70. */
  71. public function __construct( BagOStuff $cache, $cacheEpoch = '20030516000000' ) {
  72. $this->mMemc = $cache;
  73. $this->cacheEpoch = $cacheEpoch;
  74. }
  75. /**
  76. * @param WikiPage $article
  77. * @param string $hash
  78. * @return mixed|string
  79. */
  80. protected function getParserOutputKey( $article, $hash ) {
  81. global $wgRequest;
  82. // idhash seem to mean 'page id' + 'rendering hash' (r3710)
  83. $pageid = $article->getId();
  84. $renderkey = (int)( $wgRequest->getVal( 'action' ) == 'render' );
  85. $key = $this->mMemc->makeKey( 'pcache', 'idhash', "{$pageid}-{$renderkey}!{$hash}" );
  86. return $key;
  87. }
  88. /**
  89. * @param WikiPage $page
  90. * @return mixed|string
  91. */
  92. protected function getOptionsKey( $page ) {
  93. return $this->mMemc->makeKey( 'pcache', 'idoptions', $page->getId() );
  94. }
  95. /**
  96. * @param WikiPage $page
  97. * @since 1.28
  98. */
  99. public function deleteOptionsKey( $page ) {
  100. $this->mMemc->delete( $this->getOptionsKey( $page ) );
  101. }
  102. /**
  103. * Provides an E-Tag suitable for the whole page. Note that $article
  104. * is just the main wikitext. The E-Tag has to be unique to the whole
  105. * page, even if the article itself is the same, so it uses the
  106. * complete set of user options. We don't want to use the preference
  107. * of a different user on a message just because it wasn't used in
  108. * $article. For example give a Chinese interface to a user with
  109. * English preferences. That's why we take into account *all* user
  110. * options. (r70809 CR)
  111. *
  112. * @param WikiPage $article
  113. * @param ParserOptions $popts
  114. * @return string
  115. */
  116. public function getETag( $article, $popts ) {
  117. return 'W/"' . $this->getParserOutputKey( $article,
  118. $popts->optionsHash( ParserOptions::allCacheVaryingOptions(), $article->getTitle() ) ) .
  119. "--" . $article->getTouched() . '"';
  120. }
  121. /**
  122. * Retrieve the ParserOutput from ParserCache, even if it's outdated.
  123. * @param WikiPage $article
  124. * @param ParserOptions $popts
  125. * @return ParserOutput|bool False on failure
  126. */
  127. public function getDirty( $article, $popts ) {
  128. $value = $this->get( $article, $popts, true );
  129. return is_object( $value ) ? $value : false;
  130. }
  131. /**
  132. * Generates a key for caching the given article considering
  133. * the given parser options.
  134. *
  135. * @note Which parser options influence the cache key
  136. * is controlled via ParserOutput::recordOption() or
  137. * ParserOptions::addExtraKey().
  138. *
  139. * @note Used by Article to provide a unique id for the PoolCounter.
  140. * It would be preferable to have this code in get()
  141. * instead of having Article looking in our internals.
  142. *
  143. * @param WikiPage $article
  144. * @param ParserOptions $popts
  145. * @param int|bool $useOutdated One of the USE constants. For backwards
  146. * compatibility, boolean false is treated as USE_CURRENT_ONLY and
  147. * boolean true is treated as USE_ANYTHING.
  148. * @return bool|mixed|string
  149. * @since 1.30 Changed $useOutdated to an int and added the non-boolean values
  150. */
  151. public function getKey( $article, $popts, $useOutdated = self::USE_ANYTHING ) {
  152. if ( is_bool( $useOutdated ) ) {
  153. $useOutdated = $useOutdated ? self::USE_ANYTHING : self::USE_CURRENT_ONLY;
  154. }
  155. if ( $popts instanceof User ) {
  156. wfWarn( "Use of outdated prototype ParserCache::getKey( &\$article, &\$user )\n" );
  157. $popts = ParserOptions::newFromUser( $popts );
  158. }
  159. // Determine the options which affect this article
  160. $casToken = null;
  161. $optionsKey = $this->mMemc->get(
  162. $this->getOptionsKey( $article ), $casToken, BagOStuff::READ_VERIFIED );
  163. if ( $optionsKey instanceof CacheTime ) {
  164. if ( $useOutdated < self::USE_EXPIRED && $optionsKey->expired( $article->getTouched() ) ) {
  165. wfIncrStats( "pcache.miss.expired" );
  166. $cacheTime = $optionsKey->getCacheTime();
  167. wfDebugLog( "ParserCache",
  168. "Parser options key expired, touched " . $article->getTouched()
  169. . ", epoch {$this->cacheEpoch}, cached $cacheTime\n" );
  170. return false;
  171. } elseif ( $useOutdated < self::USE_OUTDATED &&
  172. $optionsKey->isDifferentRevision( $article->getLatest() )
  173. ) {
  174. wfIncrStats( "pcache.miss.revid" );
  175. $revId = $article->getLatest();
  176. $cachedRevId = $optionsKey->getCacheRevisionId();
  177. wfDebugLog( "ParserCache",
  178. "ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n"
  179. );
  180. return false;
  181. }
  182. // $optionsKey->mUsedOptions is set by save() by calling ParserOutput::getUsedOptions()
  183. $usedOptions = $optionsKey->mUsedOptions;
  184. wfDebug( "Parser cache options found.\n" );
  185. } else {
  186. if ( $useOutdated < self::USE_ANYTHING ) {
  187. return false;
  188. }
  189. $usedOptions = ParserOptions::allCacheVaryingOptions();
  190. }
  191. return $this->getParserOutputKey(
  192. $article,
  193. $popts->optionsHash( $usedOptions, $article->getTitle() )
  194. );
  195. }
  196. /**
  197. * Retrieve the ParserOutput from ParserCache.
  198. * false if not found or outdated.
  199. *
  200. * @param WikiPage|Article $article
  201. * @param ParserOptions $popts
  202. * @param bool $useOutdated (default false)
  203. *
  204. * @return ParserOutput|bool False on failure
  205. */
  206. public function get( $article, $popts, $useOutdated = false ) {
  207. $canCache = $article->checkTouched();
  208. if ( !$canCache ) {
  209. // It's a redirect now
  210. return false;
  211. }
  212. $touched = $article->getTouched();
  213. $parserOutputKey = $this->getKey( $article, $popts,
  214. $useOutdated ? self::USE_OUTDATED : self::USE_CURRENT_ONLY
  215. );
  216. if ( $parserOutputKey === false ) {
  217. wfIncrStats( 'pcache.miss.absent' );
  218. return false;
  219. }
  220. $casToken = null;
  221. /** @var ParserOutput $value */
  222. $value = $this->mMemc->get( $parserOutputKey, $casToken, BagOStuff::READ_VERIFIED );
  223. if ( !$value ) {
  224. wfDebug( "ParserOutput cache miss.\n" );
  225. wfIncrStats( "pcache.miss.absent" );
  226. return false;
  227. }
  228. wfDebug( "ParserOutput cache found.\n" );
  229. $wikiPage = method_exists( $article, 'getPage' )
  230. ? $article->getPage()
  231. : $article;
  232. if ( !$useOutdated && $value->expired( $touched ) ) {
  233. wfIncrStats( "pcache.miss.expired" );
  234. $cacheTime = $value->getCacheTime();
  235. wfDebugLog( "ParserCache",
  236. "ParserOutput key expired, touched $touched, "
  237. . "epoch {$this->cacheEpoch}, cached $cacheTime\n" );
  238. $value = false;
  239. } elseif ( !$useOutdated && $value->isDifferentRevision( $article->getLatest() ) ) {
  240. wfIncrStats( "pcache.miss.revid" );
  241. $revId = $article->getLatest();
  242. $cachedRevId = $value->getCacheRevisionId();
  243. wfDebugLog( "ParserCache",
  244. "ParserOutput key is for an old revision, latest $revId, cached $cachedRevId\n"
  245. );
  246. $value = false;
  247. } elseif (
  248. Hooks::run( 'RejectParserCacheValue', [ $value, $wikiPage, $popts ] ) === false
  249. ) {
  250. wfIncrStats( 'pcache.miss.rejected' );
  251. wfDebugLog( "ParserCache",
  252. "ParserOutput key valid, but rejected by RejectParserCacheValue hook handler.\n"
  253. );
  254. $value = false;
  255. } else {
  256. wfIncrStats( "pcache.hit" );
  257. }
  258. return $value;
  259. }
  260. /**
  261. * @param ParserOutput $parserOutput
  262. * @param WikiPage $page
  263. * @param ParserOptions $popts
  264. * @param string $cacheTime Time when the cache was generated
  265. * @param int $revId Revision ID that was parsed
  266. */
  267. public function save( $parserOutput, $page, $popts, $cacheTime = null, $revId = null ) {
  268. $expire = $parserOutput->getCacheExpiry();
  269. if ( $expire > 0 && !$this->mMemc instanceof EmptyBagOStuff ) {
  270. $cacheTime = $cacheTime ?: wfTimestampNow();
  271. if ( !$revId ) {
  272. $revision = $page->getRevision();
  273. $revId = $revision ? $revision->getId() : null;
  274. }
  275. $optionsKey = new CacheTime;
  276. $optionsKey->mUsedOptions = $parserOutput->getUsedOptions();
  277. $optionsKey->updateCacheExpiry( $expire );
  278. $optionsKey->setCacheTime( $cacheTime );
  279. $parserOutput->setCacheTime( $cacheTime );
  280. $optionsKey->setCacheRevisionId( $revId );
  281. $parserOutput->setCacheRevisionId( $revId );
  282. $parserOutputKey = $this->getParserOutputKey( $page,
  283. $popts->optionsHash( $optionsKey->mUsedOptions, $page->getTitle() ) );
  284. // Save the timestamp so that we don't have to load the revision row on view
  285. $parserOutput->setTimestamp( $page->getTimestamp() );
  286. $msg = "Saved in parser cache with key $parserOutputKey" .
  287. " and timestamp $cacheTime" .
  288. " and revision id $revId" .
  289. "\n";
  290. $parserOutput->mText .= "\n<!-- $msg -->\n";
  291. wfDebug( $msg );
  292. // Save the parser output
  293. $this->mMemc->set( $parserOutputKey, $parserOutput, $expire );
  294. // ...and its pointer
  295. $this->mMemc->set( $this->getOptionsKey( $page ), $optionsKey, $expire );
  296. Hooks::run(
  297. 'ParserCacheSaveComplete',
  298. [ $this, $parserOutput, $page->getTitle(), $popts, $revId ]
  299. );
  300. } elseif ( $expire <= 0 ) {
  301. wfDebug( "Parser output was marked as uncacheable and has not been saved.\n" );
  302. }
  303. }
  304. /**
  305. * Get the backend BagOStuff instance that
  306. * powers the parser cache
  307. *
  308. * @since 1.30
  309. * @return BagOStuff
  310. */
  311. public function getCacheStorage() {
  312. return $this->mMemc;
  313. }
  314. }