MediaWiki  master
LinkCache.php
Go to the documentation of this file.
1 <?php
30 use Psr\Log\LoggerAwareInterface;
31 use Psr\Log\LoggerInterface;
32 use Psr\Log\NullLogger;
36 
42 class LinkCache implements LoggerAwareInterface {
44  private $entries;
46  private $wanCache;
48  private $titleFormatter;
50  private $nsInfo;
52  private $loadBalancer;
54  private $logger;
55 
57  private const MAX_SIZE = 10000;
58 
60  private const ROW = 0;
62  private const FLAGS = 1;
63 
70  public function __construct(
71  TitleFormatter $titleFormatter,
72  WANObjectCache $cache,
73  NamespaceInfo $nsInfo,
74  ILoadBalancer $loadBalancer = null
75  ) {
76  $this->entries = new MapCacheLRU( self::MAX_SIZE );
77  $this->wanCache = $cache;
78  $this->titleFormatter = $titleFormatter;
79  $this->nsInfo = $nsInfo;
80  $this->loadBalancer = $loadBalancer;
81  $this->logger = new NullLogger();
82  }
83 
87  public function setLogger( LoggerInterface $logger ) {
88  $this->logger = $logger;
89  }
90 
96  private function getCacheKey( $page, $passThrough = false ) {
97  if ( is_string( $page ) ) {
98  if ( $passThrough ) {
99  return $page;
100  } else {
101  throw new InvalidArgumentException( 'They key may not be given as a string here' );
102  }
103  }
104 
105  if ( is_array( $page ) ) {
106  $namespace = $page['page_namespace'];
107  $dbkey = $page['page_title'];
108  return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
109  }
110 
111  if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
112  // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
113  $this->logger->info(
114  'cross-wiki page reference',
115  [
116  'page-wiki' => $page->getWikiId(),
117  'page-reference' => $this->titleFormatter->getFullText( $page )
118  ]
119  );
120  return null;
121  }
122 
123  if ( $page instanceof PageIdentity && !$page->canExist() ) {
124  // Non-proper page, perhaps a special page or interwiki link or relative section link.
125  $this->logger->warning(
126  'non-proper page reference: {page-reference}',
127  [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
128  );
129  return null;
130  }
131 
132  if ( $page instanceof LinkTarget
133  && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
134  ) {
135  // Interwiki link or relative section link. These do not have a page ID, so they
136  // can neither be "good" nor "bad" in the sense of this class.
137  $this->logger->warning(
138  'link to non-proper page: {page-link}',
139  [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
140  );
141  return null;
142  }
143 
144  return $this->titleFormatter->getPrefixedDBkey( $page );
145  }
146 
156  public function getGoodLinkID( $page ) {
157  $key = $this->getCacheKey( $page, true );
158  if ( $key === null ) {
159  return 0;
160  }
161 
162  $entry = $this->entries->get( $key );
163  if ( !$entry ) {
164  return 0;
165  }
166 
167  $row = $entry[self::ROW];
168 
169  return $row ? (int)$row->page_id : 0;
170  }
171 
184  public function getGoodLinkFieldObj( $page, string $field ) {
185  $key = $this->getCacheKey( $page );
186  if ( $key === null ) {
187  return null;
188  }
189 
190  $entry = $this->entries->get( $key );
191  if ( !$entry ) {
192  return null;
193  }
194 
195  $row = $entry[self::ROW];
196  if ( !$row ) {
197  return null;
198  }
199 
200  switch ( $field ) {
201  case 'id':
202  return (int)$row->page_id;
203  case 'length':
204  return (int)$row->page_len;
205  case 'redirect':
206  return (int)$row->page_is_redirect;
207  case 'revision':
208  return (int)$row->page_latest;
209  case 'model':
210  return !empty( $row->page_content_model )
211  ? (string)$row->page_content_model
212  : null;
213  case 'lang':
214  return !empty( $row->page_lang )
215  ? (string)$row->page_lang
216  : null;
217  default:
218  throw new InvalidArgumentException( "Unknown field: $field" );
219  }
220  }
221 
231  public function isBadLink( $page ) {
232  $key = $this->getCacheKey( $page, true );
233  if ( $key === null ) {
234  return false;
235  }
236 
237  $entry = $this->entries->get( $key );
238 
239  return ( $entry && !$entry[self::ROW] );
240  }
241 
257  public function addGoodLinkObj( $id, $page, $len = -1, $redir = null,
258  $revision = 0, $model = null, $lang = null
259  ) {
260  wfDeprecated( __METHOD__, '1.38' );
261  $this->addGoodLinkObjFromRow( $page, (object)[
262  'page_id' => (int)$id,
263  'page_namespace' => $page->getNamespace(),
264  'page_title' => $page->getDBkey(),
265  'page_len' => (int)$len,
266  'page_is_redirect' => (int)$redir,
267  'page_latest' => (int)$revision,
268  'page_content_model' => $model ? (string)$model : null,
269  'page_lang' => $lang ? (string)$lang : null,
270  'page_is_new' => 0,
271  'page_touched' => '',
272  ] );
273  }
274 
291  public function addGoodLinkObjFromRow(
292  $page,
293  stdClass $row,
294  int $queryFlags = IDBAccessObject::READ_NORMAL
295  ) {
296  $key = $this->getCacheKey( $page );
297  if ( $key === null ) {
298  return;
299  }
300 
301  foreach ( self::getSelectFields() as $field ) {
302  if ( !property_exists( $row, $field ) ) {
303  throw new InvalidArgumentException( "Missing field: $field" );
304  }
305  }
306 
307  $this->entries->set( $key, [ self::ROW => $row, self::FLAGS => $queryFlags ] );
308  }
309 
324  public function addBadLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
325  $key = $this->getCacheKey( $page );
326  if ( $key === null ) {
327  return;
328  }
329 
330  $this->entries->set( $key, [ self::ROW => null, self::FLAGS => $queryFlags ] );
331  }
332 
341  public function clearBadLink( $page ) {
342  $key = $this->getCacheKey( $page, true );
343  if ( $key === null ) {
344  return;
345  }
346 
347  $entry = $this->entries->get( $key );
348  if ( $entry && !$entry[self::ROW] ) {
349  $this->entries->clear( $key );
350  }
351  }
352 
361  public function clearLink( $page ) {
362  $key = $this->getCacheKey( $page );
363  if ( $key !== null ) {
364  $this->entries->clear( $key );
365  }
366  }
367 
374  public static function getSelectFields() {
375  $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig()
376  ->get( MainConfigNames::PageLanguageUseDB );
377 
378  $fields = array_merge(
379  PageStoreRecord::REQUIRED_FIELDS,
380  [
381  'page_len',
382  'page_content_model',
383  ]
384  );
385 
386  if ( $pageLanguageUseDB ) {
387  $fields[] = 'page_lang';
388  }
389 
390  return $fields;
391  }
392 
407  public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
408  $row = $this->getGoodLinkRow(
409  $page->getNamespace(),
410  $page->getDBkey(),
411  [ $this, 'fetchPageRow' ],
412  $queryFlags
413  );
414 
415  return $row ? (int)$row->page_id : 0;
416  }
417 
426  private function getGoodLinkRowInternal(
427  TitleValue $link,
428  callable $fetchCallback = null,
429  int $queryFlags = IDBAccessObject::READ_NORMAL
430  ): array {
431  $callerShouldAddGoodLink = false;
432 
433  $key = $this->getCacheKey( $link );
434  if ( $key === null ) {
435  return [ $callerShouldAddGoodLink, null ];
436  }
437 
438  $ns = $link->getNamespace();
439  $dbkey = $link->getDBkey();
440 
441  $entry = $this->entries->get( $key );
442  if ( $entry && $entry[self::FLAGS] >= $queryFlags ) {
443  return [ $callerShouldAddGoodLink, $entry[self::ROW] ?: null ];
444  }
445 
446  if ( !$fetchCallback ) {
447  return [ $callerShouldAddGoodLink, null ];
448  }
449 
450  $callerShouldAddGoodLink = true;
451 
452  $wanCacheKey = $this->getPersistentCacheKey( $link );
453  if ( $wanCacheKey !== null && !( $queryFlags & IDBAccessObject::READ_LATEST ) ) {
454  // Some pages are often transcluded heavily, so use persistent caching
455  $row = $this->wanCache->getWithSetCallback(
456  $wanCacheKey,
457  WANObjectCache::TTL_DAY,
458  function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
459  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
460  $setOpts += Database::getCacheSetOptions( $dbr );
461 
462  $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
463  $mtime = $row ? (int)wfTimestamp( TS_UNIX, $row->page_touched ) : false;
464  $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
465 
466  return $row;
467  }
468  );
469  } else {
470  // No persistent caching needed, but we can still use the callback.
471  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
472  $dbr = $this->loadBalancer->getConnectionRef( $mode );
473  $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
474  }
475 
476  return [ $callerShouldAddGoodLink, $row ?: null ];
477  }
478 
493  public function getGoodLinkRow(
494  int $ns,
495  string $dbkey,
496  callable $fetchCallback = null,
497  int $queryFlags = IDBAccessObject::READ_NORMAL
498  ): ?stdClass {
499  $link = TitleValue::tryNew( $ns, $dbkey );
500  if ( $link === null ) {
501  return null;
502  }
503 
504  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
505  $link,
506  $fetchCallback,
507  $queryFlags
508  );
509 
510  if ( $row ) {
511  if ( $shouldAddGoodLink ) {
512  try {
513  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
514  } catch ( InvalidArgumentException $e ) {
515  // a field is missing from $row; maybe we used a cache?; invalidate it and try again
516  $this->invalidateTitle( $link );
517  [ , $row ] = $this->getGoodLinkRowInternal(
518  $link,
519  $fetchCallback,
520  $queryFlags
521  );
522  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
523  }
524  }
525  } else {
526  $this->addBadLinkObj( $link );
527  }
528 
529  return $row ?: null;
530  }
531 
536  private function getPersistentCacheKey( $page ) {
537  // if no key can be derived, the page isn't cacheable
538  if ( $this->getCacheKey( $page ) === null ) {
539  return null;
540  }
541 
542  if ( !$this->usePersistentCache( $page ) ) {
543  return null;
544  }
545 
546  return $this->wanCache->makeKey(
547  'page',
548  $page->getNamespace(),
549  sha1( $page->getDBkey()
550  ) );
551  }
552 
557  private function usePersistentCache( $pageOrNamespace ) {
558  $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
559  if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
560  return true;
561  }
562  // Focus on transcluded pages more than the main content
563  if ( $this->nsInfo->isContent( $ns ) ) {
564  return false;
565  }
566  // Non-talk extension namespaces (e.g. NS_MODULE)
567  return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
568  }
569 
577  private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
578  $queryBuilder = $db->newSelectQueryBuilder()
579  ->select( self::getSelectFields() )
580  ->from( 'page' )
581  ->where( [ 'page_namespace' => $ns, 'page_title' => $dbkey ] )
582  ->options( $options );
583 
584  return $queryBuilder->caller( __METHOD__ )->fetchRow();
585  }
586 
594  public function invalidateTitle( $page ) {
595  $wanCacheKey = $this->getPersistentCacheKey( $page );
596  if ( $wanCacheKey !== null ) {
597  $this->wanCache->delete( $wanCacheKey );
598  }
599 
600  $this->clearLink( $page );
601  }
602 
606  public function clear() {
607  $this->entries->clear();
608  }
609 }
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
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:42
__construct(TitleFormatter $titleFormatter, WANObjectCache $cache, NamespaceInfo $nsInfo, ILoadBalancer $loadBalancer=null)
Definition: LinkCache.php:70
isBadLink( $page)
Check if a page is known to be missing based on the process cache.
Definition: LinkCache.php:231
clear()
Clears cache.
Definition: LinkCache.php:606
invalidateTitle( $page)
Purge the persistent link cache for a title.
Definition: LinkCache.php:594
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:407
clearLink( $page)
Clear information about a page from the process cache.
Definition: LinkCache.php:361
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about an existing page to the process cache.
Definition: LinkCache.php:291
clearBadLink( $page)
Clear information about a page being missing from the process cache.
Definition: LinkCache.php:341
addGoodLinkObj( $id, $page, $len=-1, $redir=null, $revision=0, $model=null, $lang=null)
Add information about an existing page to the process cache.
Definition: LinkCache.php:257
addBadLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about a missing page to the process cache.
Definition: LinkCache.php:324
getGoodLinkID( $page)
Get the ID of a page known to the process cache.
Definition: LinkCache.php:156
setLogger(LoggerInterface $logger)
Definition: LinkCache.php:87
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:493
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:374
getGoodLinkFieldObj( $page, string $field)
Get the field of a page known to the process cache.
Definition: LinkCache.php:184
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
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 a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
getDBkey()
Get the main part of the link target, in canonical database form.
Definition: TitleValue.php:210
getNamespace()
Get the namespace index.
Definition: TitleValue.php:194
Multi-datacenter aware caching interface.
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
if(!isset( $args[0])) $lang