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 $goodLinks;
46  private $badLinks;
48  private $wanCache;
49 
51  private $titleFormatter;
52 
54  private $nsInfo;
55 
57  private $loadBalancer;
58 
60  private $logger;
61 
66  private const MAX_SIZE = 10000;
67 
74  public function __construct(
75  TitleFormatter $titleFormatter,
77  NamespaceInfo $nsInfo,
78  ILoadBalancer $loadBalancer = null
79  ) {
80  $this->goodLinks = new MapCacheLRU( self::MAX_SIZE );
81  $this->badLinks = new MapCacheLRU( self::MAX_SIZE );
82  $this->wanCache = $cache;
83  $this->titleFormatter = $titleFormatter;
84  $this->nsInfo = $nsInfo;
85  $this->loadBalancer = $loadBalancer;
86  $this->logger = new NullLogger();
87  }
88 
92  public function setLogger( LoggerInterface $logger ) {
93  $this->logger = $logger;
94  }
95 
102  private function getCacheKey( $page, $passThrough = false ) {
103  if ( is_string( $page ) ) {
104  if ( $passThrough ) {
105  return $page;
106  } else {
107  throw new InvalidArgumentException( 'They key may not be given as a string here' );
108  }
109  }
110 
111  if ( is_array( $page ) ) {
112  $namespace = $page['page_namespace'];
113  $dbkey = $page['page_title'];
114  return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
115  }
116 
117  if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
118  // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
119  $this->logger->info(
120  'cross-wiki page reference',
121  [
122  'page-wiki' => $page->getWikiId(),
123  'page-reference' => $this->titleFormatter->getFullText( $page )
124  ]
125  );
126  return null;
127  }
128 
129  if ( $page instanceof PageIdentity && !$page->canExist() ) {
130  // Non-proper page, perhaps a special page or interwiki link or relative section link.
131  $this->logger->warning(
132  'non-proper page reference: {page-reference}',
133  [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
134  );
135  return null;
136  }
137 
138  if ( $page instanceof LinkTarget
139  && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
140  ) {
141  // Interwiki link or relative section link. These do not have a page ID, so they
142  // can neither be "good" nor "bad" in the sense of this class.
143  $this->logger->warning(
144  'link to non-proper page: {page-link}',
145  [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
146  );
147  return null;
148  }
149 
150  return $this->titleFormatter->getPrefixedDBkey( $page );
151  }
152 
162  public function getGoodLinkID( $page ) {
163  $key = $this->getCacheKey( $page, true );
164 
165  if ( $key === null ) {
166  return 0;
167  }
168 
169  [ $row ] = $this->goodLinks->get( $key );
170 
171  return $row ? (int)$row->page_id : 0;
172  }
173 
186  public function getGoodLinkFieldObj( $page, string $field ) {
187  $key = $this->getCacheKey( $page );
188  if ( $key === null ) {
189  return null;
190  }
191 
192  if ( $this->isBadLink( $key ) ) {
193  return null;
194  }
195 
196  [ $row ] = $this->goodLinks->get( $key );
197 
198  if ( !$row ) {
199  return null;
200  }
201 
202  switch ( $field ) {
203  case 'id':
204  return intval( $row->page_id );
205  case 'length':
206  return intval( $row->page_len );
207  case 'redirect':
208  return intval( $row->page_is_redirect );
209  case 'revision':
210  return intval( $row->page_latest );
211  case 'model':
212  return !empty( $row->page_content_model )
213  ? strval( $row->page_content_model )
214  : null;
215  case 'lang':
216  return !empty( $row->page_lang )
217  ? strval( $row->page_lang )
218  : null;
219  default:
220  throw new InvalidArgumentException( "Unknown field: $field" );
221  }
222  }
223 
233  public function isBadLink( $page ) {
234  $key = $this->getCacheKey( $page, true );
235  if ( $key === null ) {
236  return false;
237  }
238 
239  return $this->badLinks->has( $key );
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 
288  public function addGoodLinkObjFromRow(
289  $page,
290  stdClass $row,
291  int $queryFlags = IDBAccessObject::READ_NORMAL
292  ) {
293  foreach ( self::getSelectFields() as $field ) {
294  if ( !property_exists( $row, $field ) ) {
295  throw new InvalidArgumentException( "Missing field: $field" );
296  }
297  }
298 
299  $key = $this->getCacheKey( $page );
300  if ( $key === null ) {
301  return;
302  }
303 
304  $this->goodLinks->set( $key, [ $row, $queryFlags ] );
305  $this->badLinks->clear( $key );
306  }
307 
314  public function addBadLinkObj( $page ) {
315  $key = $this->getCacheKey( $page );
316  if ( $key !== null && !$this->isBadLink( $key ) ) {
317  $this->badLinks->set( $key, 1 );
318  $this->goodLinks->clear( $key );
319  }
320  }
321 
328  public function clearBadLink( $page ) {
329  $key = $this->getCacheKey( $page, true );
330 
331  if ( $key !== null ) {
332  $this->badLinks->clear( $key );
333  }
334  }
335 
342  public function clearLink( $page ) {
343  $key = $this->getCacheKey( $page );
344 
345  if ( $key !== null ) {
346  $this->badLinks->clear( $key );
347  $this->goodLinks->clear( $key );
348  }
349  }
350 
357  public static function getSelectFields() {
358  $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig()
359  ->get( MainConfigNames::PageLanguageUseDB );
360 
361  $fields = array_merge(
362  PageStoreRecord::REQUIRED_FIELDS,
363  [
364  'page_len',
365  'page_content_model',
366  ]
367  );
368 
369  if ( $pageLanguageUseDB ) {
370  $fields[] = 'page_lang';
371  }
372 
373  return $fields;
374  }
375 
390  public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
391  $row = $this->getGoodLinkRow(
392  $page->getNamespace(),
393  $page->getDBkey(),
394  [ $this, 'fetchPageRow' ],
395  $queryFlags
396  );
397 
398  return $row ? (int)$row->page_id : 0;
399  }
400 
409  private function getGoodLinkRowInternal(
410  ?TitleValue $link,
411  callable $fetchCallback = null,
412  int $queryFlags = IDBAccessObject::READ_NORMAL
413  ): array {
414  $key = $link ? $this->getCacheKey( $link ) : null;
415  if ( $key === null ) {
416  return [ false, false ];
417  }
418 
419  $ns = $link->getNamespace();
420  $dbkey = $link->getDBkey();
421  $callerShouldAddGoodLink = false;
422 
423  $forUpdate = $queryFlags & IDBAccessObject::READ_LATEST;
424 
425  if ( !$forUpdate && $this->isBadLink( $key ) ) {
426  return [ $callerShouldAddGoodLink, false ];
427  }
428 
429  [ $row, $rowFlags ] = $this->goodLinks->get( $key );
430  if ( $row && $rowFlags >= $queryFlags ) {
431  return [ $callerShouldAddGoodLink, $row ];
432  }
433 
434  if ( !$fetchCallback ) {
435  return [ $callerShouldAddGoodLink, false ];
436  }
437 
438  $callerShouldAddGoodLink = true;
439  if ( $this->usePersistentCache( $ns ) && !$forUpdate ) {
440  // Some pages are often transcluded heavily, so use persistent caching
441  $wanCacheKey = $this->wanCache->makeKey( 'page', $ns, sha1( $dbkey ) );
442 
443  $row = $this->wanCache->getWithSetCallback(
444  $wanCacheKey,
445  WANObjectCache::TTL_DAY,
446  function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
447  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
448  $setOpts += Database::getCacheSetOptions( $dbr );
449 
450  $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
451  $mtime = $row ? (int)wfTimestamp( TS_UNIX, $row->page_touched ) : false;
452  $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
453 
454  return $row;
455  }
456  );
457  } else {
458  // No persistent caching needed, but we can still use the callback.
459  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
460  $dbr = $this->loadBalancer->getConnectionRef( $mode );
461  $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
462  }
463 
464  return [ $callerShouldAddGoodLink, $row ];
465  }
466 
481  public function getGoodLinkRow(
482  int $ns,
483  string $dbkey,
484  callable $fetchCallback = null,
485  int $queryFlags = IDBAccessObject::READ_NORMAL
486  ): ?stdClass {
487  $link = TitleValue::tryNew( $ns, $dbkey );
488  if ( $link === null ) {
489  return null;
490  }
491  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
492  $link,
493  $fetchCallback,
494  $queryFlags
495  );
496 
497  if ( $row ) {
498  if ( $shouldAddGoodLink ) {
499  try {
500  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
501  } catch ( InvalidArgumentException $e ) {
502  // a field is missing from $row; maybe we used a cache?; invalidate it and try again
503  $this->invalidateTitle( $link );
504  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
505  $link,
506  $fetchCallback,
507  $queryFlags
508  );
509  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
510  }
511  }
512  } else {
513  $this->addBadLinkObj( $link );
514  }
515 
516  return $row ?: null;
517  }
518 
526  public function getMutableCacheKeys( WANObjectCache $cache, $page ) {
527  $key = $this->getCacheKey( $page );
528  // if no key can be derived, the page isn't cacheable
529  if ( $key === null ) {
530  return [];
531  }
532 
533  if ( $this->usePersistentCache( $page ) ) {
534  return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ];
535  }
536 
537  return [];
538  }
539 
545  private function usePersistentCache( $pageOrNamespace ) {
546  $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
547  if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
548  return true;
549  }
550  // Focus on transcluded pages more than the main content
551  if ( $this->nsInfo->isContent( $ns ) ) {
552  return false;
553  }
554  // Non-talk extension namespaces (e.g. NS_MODULE)
555  return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
556  }
557 
566  private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
567  $fields = self::getSelectFields();
568  if ( $this->usePersistentCache( $ns ) ) {
569  $fields[] = 'page_touched';
570  }
571 
572  return $db->selectRow(
573  'page',
574  $fields,
575  [ 'page_namespace' => $ns, 'page_title' => $dbkey ],
576  __METHOD__,
577  $options
578  );
579  }
580 
588  public function invalidateTitle( $page ) {
589  if ( $this->usePersistentCache( $page ) ) {
590  $cache = $this->wanCache;
591  $cache->delete(
592  $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) )
593  );
594  }
595 
596  $this->clearLink( $page );
597  }
598 
602  public function clear() {
603  $this->goodLinks->clear();
604  $this->badLinks->clear();
605  }
606 
607 }
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:74
isBadLink( $page)
Returns true if the fact that this page does not exist had been added to the cache.
Definition: LinkCache.php:233
clear()
Clears cache.
Definition: LinkCache.php:602
invalidateTitle( $page)
Purge the persistent link cache for a title.
Definition: LinkCache.php:588
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:390
clearLink( $page)
Definition: LinkCache.php:342
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Same as above with better interface.
Definition: LinkCache.php:288
clearBadLink( $page)
Definition: LinkCache.php:328
addGoodLinkObj( $id, $page, $len=-1, $redir=null, $revision=0, $model=null, $lang=null)
Add information about an existing page to the cache.
Definition: LinkCache.php:257
getGoodLinkID( $page)
Returns the ID of the given page, if information about this page has been cached.
Definition: LinkCache.php:162
setLogger(LoggerInterface $logger)
Definition: LinkCache.php:92
addBadLinkObj( $page)
Definition: LinkCache.php:314
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:481
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:357
getGoodLinkFieldObj( $page, string $field)
Get a field of a page from the cache.
Definition: LinkCache.php:186
getMutableCacheKeys(WANObjectCache $cache, $page)
Definition: LinkCache.php:526
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:39
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
Create and track the database connections and transactions for a given database cluster.
$cache
Definition: mcc.php:33
const DB_REPLICA
Definition: defines.php:26
if(!isset( $args[0])) $lang