MediaWiki  master
LinkCache.php
Go to the documentation of this file.
1 <?php
33 use Psr\Log\LoggerAwareInterface;
34 use Psr\Log\LoggerInterface;
35 use Psr\Log\NullLogger;
39 
45 class LinkCache implements LoggerAwareInterface {
47  private $entries;
49  private $wanCache;
51  private $titleFormatter;
53  private $nsInfo;
55  private $loadBalancer;
57  private $logger;
58 
60  private const MAX_SIZE = 10000;
61 
63  private const ROW = 0;
65  private const FLAGS = 1;
66 
73  public function __construct(
74  TitleFormatter $titleFormatter,
75  WANObjectCache $cache,
76  NamespaceInfo $nsInfo,
77  ILoadBalancer $loadBalancer = null
78  ) {
79  $this->entries = new MapCacheLRU( self::MAX_SIZE );
80  $this->wanCache = $cache;
81  $this->titleFormatter = $titleFormatter;
82  $this->nsInfo = $nsInfo;
83  $this->loadBalancer = $loadBalancer;
84  $this->logger = new NullLogger();
85  }
86 
90  public function setLogger( LoggerInterface $logger ) {
91  $this->logger = $logger;
92  }
93 
99  private function getCacheKey( $page, $passThrough = false ) {
100  if ( is_string( $page ) ) {
101  if ( $passThrough ) {
102  return $page;
103  } else {
104  throw new InvalidArgumentException( 'They key may not be given as a string here' );
105  }
106  }
107 
108  if ( is_array( $page ) ) {
109  $namespace = $page['page_namespace'];
110  $dbkey = $page['page_title'];
111  return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
112  }
113 
114  if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
115  // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
116  $this->logger->info(
117  'cross-wiki page reference',
118  [
119  'page-wiki' => $page->getWikiId(),
120  'page-reference' => $this->titleFormatter->getFullText( $page )
121  ]
122  );
123  return null;
124  }
125 
126  if ( $page instanceof PageIdentity && !$page->canExist() ) {
127  // Non-proper page, perhaps a special page or interwiki link or relative section link.
128  $this->logger->warning(
129  'non-proper page reference: {page-reference}',
130  [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
131  );
132  return null;
133  }
134 
135  if ( $page instanceof LinkTarget
136  && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
137  ) {
138  // Interwiki link or relative section link. These do not have a page ID, so they
139  // can neither be "good" nor "bad" in the sense of this class.
140  $this->logger->warning(
141  'link to non-proper page: {page-link}',
142  [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
143  );
144  return null;
145  }
146 
147  return $this->titleFormatter->getPrefixedDBkey( $page );
148  }
149 
159  public function getGoodLinkID( $page ) {
160  $key = $this->getCacheKey( $page, true );
161  if ( $key === null ) {
162  return 0;
163  }
164 
165  $entry = $this->entries->get( $key );
166  if ( !$entry ) {
167  return 0;
168  }
169 
170  $row = $entry[self::ROW];
171 
172  return $row ? (int)$row->page_id : 0;
173  }
174 
187  public function getGoodLinkFieldObj( $page, string $field ) {
188  $key = $this->getCacheKey( $page );
189  if ( $key === null ) {
190  return null;
191  }
192 
193  $entry = $this->entries->get( $key );
194  if ( !$entry ) {
195  return null;
196  }
197 
198  $row = $entry[self::ROW];
199  if ( !$row ) {
200  return null;
201  }
202 
203  switch ( $field ) {
204  case 'id':
205  return (int)$row->page_id;
206  case 'length':
207  return (int)$row->page_len;
208  case 'redirect':
209  return (int)$row->page_is_redirect;
210  case 'revision':
211  return (int)$row->page_latest;
212  case 'model':
213  return !empty( $row->page_content_model )
214  ? (string)$row->page_content_model
215  : null;
216  case 'lang':
217  return !empty( $row->page_lang )
218  ? (string)$row->page_lang
219  : null;
220  default:
221  throw new InvalidArgumentException( "Unknown field: $field" );
222  }
223  }
224 
234  public function isBadLink( $page ) {
235  $key = $this->getCacheKey( $page, true );
236  if ( $key === null ) {
237  return false;
238  }
239 
240  $entry = $this->entries->get( $key );
241 
242  return ( $entry && !$entry[self::ROW] );
243  }
244 
261  public function addGoodLinkObjFromRow(
262  $page,
263  stdClass $row,
264  int $queryFlags = IDBAccessObject::READ_NORMAL
265  ) {
266  $key = $this->getCacheKey( $page );
267  if ( $key === null ) {
268  return;
269  }
270 
271  foreach ( self::getSelectFields() as $field ) {
272  if ( !property_exists( $row, $field ) ) {
273  throw new InvalidArgumentException( "Missing field: $field" );
274  }
275  }
276 
277  $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] );
278  }
279 
294  public function addBadLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
295  $key = $this->getCacheKey( $page );
296  if ( $key === null ) {
297  return;
298  }
299 
300  $this->entries->set( $key, [ self::ROW => null, self::FLAGS => $queryFlags ] );
301  }
302 
311  public function clearBadLink( $page ) {
312  $key = $this->getCacheKey( $page, true );
313  if ( $key === null ) {
314  return;
315  }
316 
317  $entry = $this->entries->get( $key );
318  if ( $entry && !$entry[self::ROW] ) {
319  $this->entries->clear( $key );
320  }
321  }
322 
331  public function clearLink( $page ) {
332  $key = $this->getCacheKey( $page );
333  if ( $key !== null ) {
334  $this->entries->clear( $key );
335  }
336  }
337 
344  public static function getSelectFields() {
345  $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig()
346  ->get( MainConfigNames::PageLanguageUseDB );
347 
348  $fields = array_merge(
349  PageStoreRecord::REQUIRED_FIELDS,
350  [
351  'page_len',
352  'page_content_model',
353  ]
354  );
355 
356  if ( $pageLanguageUseDB ) {
357  $fields[] = 'page_lang';
358  }
359 
360  return $fields;
361  }
362 
377  public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
378  $row = $this->getGoodLinkRow(
379  $page->getNamespace(),
380  $page->getDBkey(),
381  [ $this, 'fetchPageRow' ],
382  $queryFlags
383  );
384 
385  return $row ? (int)$row->page_id : 0;
386  }
387 
396  private function getGoodLinkRowInternal(
397  TitleValue $link,
398  callable $fetchCallback = null,
399  int $queryFlags = IDBAccessObject::READ_NORMAL
400  ): array {
401  $callerShouldAddGoodLink = false;
402 
403  $key = $this->getCacheKey( $link );
404  if ( $key === null ) {
405  return [ $callerShouldAddGoodLink, null ];
406  }
407 
408  $ns = $link->getNamespace();
409  $dbkey = $link->getDBkey();
410 
411  $entry = $this->entries->get( $key );
412  if ( $entry && $entry[self::FLAGS] >= $queryFlags ) {
413  return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ];
414  }
415 
416  if ( !$fetchCallback ) {
417  return [ $callerShouldAddGoodLink, null ];
418  }
419 
420  $callerShouldAddGoodLink = true;
421 
422  $wanCacheKey = $this->getPersistentCacheKey( $link );
423  if ( $wanCacheKey !== null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
424  // Some pages are often transcluded heavily, so use persistent caching
425  $row = $this->wanCache->getWithSetCallback(
426  $wanCacheKey,
427  WANObjectCache::TTL_DAY,
428  function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
429  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
430  $setOpts += Database::getCacheSetOptions( $dbr );
431 
432  $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
433  $mtime = $row ? (int)wfTimestamp( TS_UNIX, $row->page_touched ) : false;
434  $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
435 
436  return $row;
437  }
438  );
439  } else {
440  // No persistent caching needed, but we can still use the callback.
441  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
442  $dbr = $this->loadBalancer->getConnectionRef( $mode );
443  $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
444  }
445 
446  return [ $callerShouldAddGoodLink, $row ?: null ];
447  }
448 
463  public function getGoodLinkRow(
464  int $ns,
465  string $dbkey,
466  callable $fetchCallback = null,
467  int $queryFlags = IDBAccessObject::READ_NORMAL
468  ): ?stdClass {
469  $link = TitleValue::tryNew( $ns, $dbkey );
470  if ( $link === null ) {
471  return null;
472  }
473 
474  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
475  $link,
476  $fetchCallback,
477  $queryFlags
478  );
479 
480  if ( $row ) {
481  if ( $shouldAddGoodLink ) {
482  try {
483  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
484  } catch ( InvalidArgumentException $e ) {
485  // a field is missing from $row; maybe we used a cache?; invalidate it and try again
486  $this->invalidateTitle( $link );
487  [ , $row ] = $this->getGoodLinkRowInternal(
488  $link,
489  $fetchCallback,
490  $queryFlags
491  );
492  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
493  }
494  }
495  } else {
496  $this->addBadLinkObj( $link );
497  }
498 
499  return $row ?: null;
500  }
501 
506  private function getPersistentCacheKey( $page ) {
507  // if no key can be derived, the page isn't cacheable
508  if ( $this->getCacheKey( $page ) === null || !$this->usePersistentCache( $page ) ) {
509  return null;
510  }
511 
512  return $this->wanCache->makeKey(
513  'page',
514  $page->getNamespace(),
515  sha1( $page->getDBkey()
516  ) );
517  }
518 
523  private function usePersistentCache( $pageOrNamespace ) {
524  $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
525  if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
526  return true;
527  }
528  // Focus on transcluded pages more than the main content
529  if ( $this->nsInfo->isContent( $ns ) ) {
530  return false;
531  }
532  // Non-talk extension namespaces (e.g. NS_MODULE)
533  return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
534  }
535 
543  private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
544  $queryBuilder = $db->newSelectQueryBuilder()
545  ->select( self::getSelectFields() )
546  ->from( 'page' )
547  ->where( [ 'page_namespace' => $ns, 'page_title' => $dbkey ] )
548  ->options( $options );
549 
550  return $queryBuilder->caller( __METHOD__ )->fetchRow();
551  }
552 
560  public function invalidateTitle( $page ) {
561  $wanCacheKey = $this->getPersistentCacheKey( $page );
562  if ( $wanCacheKey !== null ) {
563  $this->wanCache->delete( $wanCacheKey );
564  }
565 
566  $this->clearLink( $page );
567  }
568 
572  public function clear() {
573  $this->entries->clear();
574  }
575 }
const NS_FILE
Definition: Defines.php:70
const NS_MEDIAWIKI
Definition: Defines.php:72
const NS_TEMPLATE
Definition: Defines.php:74
const NS_CATEGORY
Definition: Defines.php:78
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition: LinkCache.php:45
__construct(TitleFormatter $titleFormatter, WANObjectCache $cache, NamespaceInfo $nsInfo, ILoadBalancer $loadBalancer=null)
Definition: LinkCache.php:73
isBadLink( $page)
Check if a page is known to be missing based on the process cache.
Definition: LinkCache.php:234
clear()
Clears cache.
Definition: LinkCache.php:572
invalidateTitle( $page)
Purge the persistent link cache for a title.
Definition: LinkCache.php:560
addLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add a title to the link cache, return the page_id or zero if non-existent.
Definition: LinkCache.php:377
clearLink( $page)
Clear information about a page from the process cache.
Definition: LinkCache.php:331
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about an existing page to the process cache.
Definition: LinkCache.php:261
clearBadLink( $page)
Clear information about a page being missing from the process cache.
Definition: LinkCache.php:311
addBadLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about a missing page to the process cache.
Definition: LinkCache.php:294
getGoodLinkID( $page)
Get the ID of a page known to the process cache.
Definition: LinkCache.php:159
setLogger(LoggerInterface $logger)
Definition: LinkCache.php:90
getGoodLinkRow(int $ns, string $dbkey, callable $fetchCallback=null, int $queryFlags=IDBAccessObject::READ_NORMAL)
Returns the row for the page if the page exists (subject to race conditions).
Definition: LinkCache.php:463
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:344
getGoodLinkFieldObj( $page, string $field)
Get the field of a page known to the process cache.
Definition: LinkCache.php:187
Store key-value entries in a size-limited in-memory LRU cache.
Definition: MapCacheLRU.php:34
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Immutable data record representing an editable page on a wiki.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents the target of a wiki link.
Definition: TitleValue.php:44
getDBkey()
Get the main part of the link target, in canonical database form.
Definition: TitleValue.php:214
getNamespace()
Get the namespace index.
Definition: TitleValue.php:198
Multi-datacenter aware caching interface.
Represents the target of a wiki link.
Definition: LinkTarget.php:30
Interface for objects (potentially) representing an editable wiki page.
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
getWikiId()
Get the ID of the wiki this page belongs to.
A title formatter service for MediaWiki.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:36
This class is a delegate to ILBFactory for a given database cluster.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
const DB_REPLICA
Definition: defines.php:26