MediaWiki  master
LinkCache.php
Go to the documentation of this file.
1 <?php
29 use Psr\Log\LoggerAwareInterface;
30 use Psr\Log\LoggerInterface;
31 use Psr\Log\NullLogger;
35 
41 class LinkCache implements LoggerAwareInterface {
43  private $goodLinks;
45  private $badLinks;
47  private $wanCache;
48 
50  private $mForUpdate = false;
51 
53  private $titleFormatter;
54 
56  private $nsInfo;
57 
59  private $loadBalancer;
60 
62  private $logger;
63 
68  private const MAX_SIZE = 10000;
69 
76  public function __construct(
79  NamespaceInfo $nsInfo = null,
81  ) {
82  if ( !$nsInfo ) {
83  wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' );
84  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
85  }
86  $this->goodLinks = new MapCacheLRU( self::MAX_SIZE );
87  $this->badLinks = new MapCacheLRU( self::MAX_SIZE );
88  $this->wanCache = $cache;
89  $this->titleFormatter = $titleFormatter;
90  $this->nsInfo = $nsInfo;
91  $this->loadBalancer = $loadBalancer;
92  $this->logger = new NullLogger();
93  }
94 
98  public function setLogger( LoggerInterface $logger ) {
99  $this->logger = $logger;
100  }
101 
113  public function forUpdate( $update = null ) {
114  wfDeprecated( __METHOD__, '1.34' ); // hard deprecated since 1.37
115  return wfSetVar( $this->mForUpdate, $update );
116  }
117 
124  private function getCacheKey( $page, $passThrough = false ) {
125  if ( is_string( $page ) ) {
126  if ( $passThrough ) {
127  return $page;
128  } else {
129  throw new InvalidArgumentException( 'They key may not be given as a string here' );
130  }
131  }
132 
133  if ( is_array( $page ) ) {
134  $namespace = $page['page_namespace'];
135  $dbkey = $page['page_title'];
136  return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
137  }
138 
139  if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
140  // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
141  $this->logger->info(
142  'cross-wiki page reference',
143  [
144  'page-wiki' => $page->getWikiId(),
145  'page-reference' => $this->titleFormatter->getFullText( $page )
146  ]
147  );
148  return null;
149  }
150 
151  if ( $page instanceof PageIdentity && !$page->canExist() ) {
152  // Non-proper page, perhaps a special page or interwiki link or relative section link.
153  $this->logger->warning(
154  'non-proper page reference: {page-reference}',
155  [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
156  );
157  return null;
158  }
159 
160  if ( $page instanceof LinkTarget
161  && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
162  ) {
163  // Interwiki link or relative section link. These do not have a page ID, so they
164  // can neither be "good" nor "bad" in the sense of this class.
165  $this->logger->warning(
166  'link to non-proper page: {page-link}',
167  [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
168  );
169  return null;
170  }
171 
172  return $this->titleFormatter->getPrefixedDBkey( $page );
173  }
174 
184  public function getGoodLinkID( $page ) {
185  $key = $this->getCacheKey( $page, true );
186 
187  if ( $key === null ) {
188  return 0;
189  }
190 
191  [ $row ] = $this->goodLinks->get( $key );
192 
193  return $row ? (int)$row->page_id : 0;
194  }
195 
208  public function getGoodLinkFieldObj( $page, string $field ) {
209  $key = $this->getCacheKey( $page );
210  if ( $key === null ) {
211  return null;
212  }
213 
214  if ( $this->isBadLink( $key ) ) {
215  return null;
216  }
217 
218  [ $row ] = $this->goodLinks->get( $key );
219 
220  if ( !$row ) {
221  return null;
222  }
223 
224  switch ( $field ) {
225  case 'id':
226  return intval( $row->page_id );
227  case 'length':
228  return intval( $row->page_len );
229  case 'redirect':
230  return intval( $row->page_is_redirect );
231  case 'revision':
232  return intval( $row->page_latest );
233  case 'model':
234  return !empty( $row->page_content_model )
235  ? strval( $row->page_content_model )
236  : null;
237  case 'lang':
238  return !empty( $row->page_lang )
239  ? strval( $row->page_lang )
240  : null;
241  case 'restrictions':
242  return !empty( $row->page_restrictions )
243  ? strval( $row->page_restrictions )
244  : null;
245  default:
246  throw new InvalidArgumentException( "Unknown field: $field" );
247  }
248  }
249 
259  public function isBadLink( $page ) {
260  $key = $this->getCacheKey( $page, true );
261  if ( $key === null ) {
262  return false;
263  }
264 
265  return $this->badLinks->has( $key );
266  }
267 
283  public function addGoodLinkObj( $id, $page, $len = -1, $redir = null,
284  $revision = 0, $model = null, $lang = null
285  ) {
286  wfDeprecated( __METHOD__, '1.38' );
287  $this->addGoodLinkObjFromRow( $page, (object)[
288  'page_id' => (int)$id,
289  'page_namespace' => $page->getNamespace(),
290  'page_title' => $page->getDBkey(),
291  'page_len' => (int)$len,
292  'page_is_redirect' => (int)$redir,
293  'page_latest' => (int)$revision,
294  'page_content_model' => $model ? (string)$model : null,
295  'page_lang' => $lang ? (string)$lang : null,
296  'page_restrictions' => null,
297  'page_is_new' => 0,
298  'page_touched' => '',
299  ] );
300  }
301 
315  public function addGoodLinkObjFromRow(
316  $page,
317  stdClass $row,
318  int $queryFlags = IDBAccessObject::READ_NORMAL
319  ) {
320  foreach ( self::getSelectFields() as $field ) {
321  if ( !property_exists( $row, $field ) ) {
322  throw new InvalidArgumentException( "Missing field: $field" );
323  }
324  }
325 
326  $key = $this->getCacheKey( $page );
327  if ( $key === null ) {
328  return;
329  }
330 
331  $this->goodLinks->set( $key, [ $row, $queryFlags ] );
332  $this->badLinks->clear( $key );
333  }
334 
341  public function addBadLinkObj( $page ) {
342  $key = $this->getCacheKey( $page );
343  if ( $key !== null && !$this->isBadLink( $key ) ) {
344  $this->badLinks->set( $key, 1 );
345  $this->goodLinks->clear( $key );
346  }
347  }
348 
355  public function clearBadLink( $page ) {
356  $key = $this->getCacheKey( $page, true );
357 
358  if ( $key !== null ) {
359  $this->badLinks->clear( $key );
360  }
361  }
362 
369  public function clearLink( $page ) {
370  $key = $this->getCacheKey( $page );
371 
372  if ( $key !== null ) {
373  $this->badLinks->clear( $key );
374  $this->goodLinks->clear( $key );
375  }
376  }
377 
384  public static function getSelectFields() {
385  $pageLanguageUseDB = MediaWikiServices::getInstance()->getMainConfig()->get( 'PageLanguageUseDB' );
386 
387  $fields = array_merge(
388  PageStoreRecord::REQUIRED_FIELDS,
389  [
390  'page_len',
391  'page_restrictions',
392  'page_content_model',
393  ]
394  );
395 
396  if ( $pageLanguageUseDB ) {
397  $fields[] = 'page_lang';
398  }
399 
400  return $fields;
401  }
402 
417  public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
418  $row = $this->getGoodLinkRow(
419  $page->getNamespace(),
420  $page->getDBkey(),
421  [ $this, 'fetchPageRow' ],
422  $queryFlags
423  );
424 
425  return $row ? (int)$row->page_id : 0;
426  }
427 
436  private function getGoodLinkRowInternal(
437  ?TitleValue $link,
438  callable $fetchCallback = null,
439  int $queryFlags = IDBAccessObject::READ_NORMAL
440  ): array {
441  $key = $link ? $this->getCacheKey( $link ) : null;
442  if ( $key === null ) {
443  return [ false, false ];
444  }
445 
446  $ns = $link->getNamespace();
447  $dbkey = $link->getDBkey();
448  $callerShouldAddGoodLink = false;
449 
450  if ( $this->mForUpdate ) {
451  $queryFlags |= IDBAccessObject::READ_LATEST;
452  }
453  $forUpdate = $queryFlags & IDBAccessObject::READ_LATEST;
454 
455  if ( !$forUpdate && $this->isBadLink( $key ) ) {
456  return [ $callerShouldAddGoodLink, false ];
457  }
458 
459  [ $row, $rowFlags ] = $this->goodLinks->get( $key );
460  if ( $row && $rowFlags >= $queryFlags ) {
461  return [ $callerShouldAddGoodLink, $row ];
462  }
463 
464  if ( !$fetchCallback ) {
465  return [ $callerShouldAddGoodLink, false ];
466  }
467 
468  $callerShouldAddGoodLink = true;
469  if ( $this->usePersistentCache( $ns ) && !$forUpdate ) {
470  // Some pages are often transcluded heavily, so use persistent caching
471  $wanCacheKey = $this->wanCache->makeKey( 'page', $ns, sha1( $dbkey ) );
472 
473  $row = $this->wanCache->getWithSetCallback(
474  $wanCacheKey,
475  WANObjectCache::TTL_DAY,
476  function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
477  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
478  $setOpts += Database::getCacheSetOptions( $dbr );
479 
480  $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
481  $mtime = $row ? (int)wfTimestamp( TS_UNIX, $row->page_touched ) : false;
482  $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
483 
484  return $row;
485  }
486  );
487  } else {
488  // No persistent caching needed, but we can still use the callback.
489  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
490  $dbr = $this->loadBalancer->getConnectionRef( $mode );
491  $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
492  }
493 
494  return [ $callerShouldAddGoodLink, $row ];
495  }
496 
511  public function getGoodLinkRow(
512  int $ns,
513  string $dbkey,
514  callable $fetchCallback = null,
515  int $queryFlags = IDBAccessObject::READ_NORMAL
516  ): ?stdClass {
517  $link = TitleValue::tryNew( $ns, $dbkey );
518  if ( $link === null ) {
519  return null;
520  }
521  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
522  $link,
523  $fetchCallback,
524  $queryFlags
525  );
526 
527  if ( $row ) {
528  if ( $shouldAddGoodLink ) {
529  try {
530  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
531  } catch ( InvalidArgumentException $e ) {
532  // a field is missing from $row; maybe we used a cache?; invalidate it and try again
533  $this->invalidateTitle( $link );
534  [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
535  $link,
536  $fetchCallback,
537  $queryFlags
538  );
539  $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
540  }
541  }
542  } else {
543  $this->addBadLinkObj( $link );
544  }
545 
546  return $row ?: null;
547  }
548 
556  public function getMutableCacheKeys( WANObjectCache $cache, $page ) {
557  $key = $this->getCacheKey( $page );
558  // if no key can be derived, the page isn't cacheable
559  if ( $key === null ) {
560  return [];
561  }
562 
563  if ( $this->usePersistentCache( $page ) ) {
564  return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ];
565  }
566 
567  return [];
568  }
569 
575  private function usePersistentCache( $pageOrNamespace ) {
576  $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
577  if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
578  return true;
579  }
580  // Focus on transcluded pages more than the main content
581  if ( $this->nsInfo->isContent( $ns ) ) {
582  return false;
583  }
584  // Non-talk extension namespaces (e.g. NS_MODULE)
585  return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
586  }
587 
596  private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
597  $fields = self::getSelectFields();
598  if ( $this->usePersistentCache( $ns ) ) {
599  $fields[] = 'page_touched';
600  }
601 
602  return $db->selectRow(
603  'page',
604  $fields,
605  [ 'page_namespace' => $ns, 'page_title' => $dbkey ],
606  __METHOD__,
607  $options
608  );
609  }
610 
618  public function invalidateTitle( $page ) {
619  if ( $this->usePersistentCache( $page ) ) {
621  $cache->delete(
622  $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) )
623  );
624  }
625 
626  $this->clearLink( $page );
627  }
628 
632  public function clear() {
633  $this->goodLinks->clear();
634  $this->badLinks->clear();
635  }
636 
637 }
LinkCache\__construct
__construct(TitleFormatter $titleFormatter, WANObjectCache $cache, NamespaceInfo $nsInfo=null, ILoadBalancer $loadBalancer=null)
Definition: LinkCache.php:76
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
LinkCache
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition: LinkCache.php:41
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
LinkCache\clearBadLink
clearBadLink( $page)
Definition: LinkCache.php:355
LinkCache\addLinkObj
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:417
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
LinkCache\$goodLinks
MapCacheLRU $goodLinks
Definition: LinkCache.php:43
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
wfSetVar
wfSetVar(&$dest, $source, $force=false)
Sets dest to source and returns the original value of dest If source is NULL, it just returns the val...
Definition: GlobalFunctions.php:1496
LinkCache\setLogger
setLogger(LoggerInterface $logger)
Definition: LinkCache.php:98
LinkCache\$logger
LoggerInterface $logger
Definition: LinkCache.php:62
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1649
LinkCache\invalidateTitle
invalidateTitle( $page)
Purge the persistent link cache for a title.
Definition: LinkCache.php:618
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
LinkCache\forUpdate
forUpdate( $update=null)
General accessor to get/set whether the primary DB should be used.
Definition: LinkCache.php:113
Page\PageReference
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
Definition: PageReference.php:49
LinkCache\getSelectFields
static getSelectFields()
Fields that LinkCache needs to select.
Definition: LinkCache.php:384
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
LinkCache\getCacheKey
getCacheKey( $page, $passThrough=false)
Definition: LinkCache.php:124
Page\PageStoreRecord
Immutable data record representing an editable page on a wiki.
Definition: PageStoreRecord.php:33
LinkCache\$badLinks
MapCacheLRU $badLinks
Definition: LinkCache.php:45
$dbr
$dbr
Definition: testCompression.php:54
LinkCache\isBadLink
isBadLink( $page)
Returns true if the fact that this page does not exist had been added to the cache.
Definition: LinkCache.php:259
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
TitleValue\getDBkey
getDBkey()
Get the main part of the link target, in canonical database form.
Definition: TitleValue.php:210
TitleValue\tryNew
static tryNew( $namespace, $title, $fragment='', $interwiki='')
Constructs a TitleValue, or returns null if the parameters are not valid.
Definition: TitleValue.php:80
LinkCache\$titleFormatter
TitleFormatter $titleFormatter
Definition: LinkCache.php:53
LinkCache\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache, $page)
Definition: LinkCache.php:556
NS_TEMPLATE
const NS_TEMPLATE
Definition: Defines.php:74
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:36
LinkCache\getGoodLinkFieldObj
getGoodLinkFieldObj( $page, string $field)
Get a field of a page from the cache.
Definition: LinkCache.php:208
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
LinkCache\MAX_SIZE
const MAX_SIZE
How many Titles to store.
Definition: LinkCache.php:68
LinkCache\clearLink
clearLink( $page)
Definition: LinkCache.php:369
Page\PageReference\getWikiId
getWikiId()
Get the ID of the wiki this page belongs to.
LinkCache\$nsInfo
NamespaceInfo $nsInfo
Definition: LinkCache.php:56
LinkCache\$loadBalancer
ILoadBalancer null $loadBalancer
Definition: LinkCache.php:59
LinkCache\$mForUpdate
bool $mForUpdate
Definition: LinkCache.php:50
LinkCache\fetchPageRow
fetchPageRow(IDatabase $db, int $ns, string $dbkey, $options=[])
Definition: LinkCache.php:596
Wikimedia\Rdbms\IDatabase\selectRow
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
LinkCache\usePersistentCache
usePersistentCache( $pageOrNamespace)
Definition: LinkCache.php:575
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:131
LinkCache\getGoodLinkRow
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:511
TitleValue\getNamespace
getNamespace()
Get the namespace index.
Definition: TitleValue.php:194
LinkCache\addGoodLinkObj
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:283
$cache
$cache
Definition: mcc.php:33
LinkCache\clear
clear()
Clears cache.
Definition: LinkCache.php:632
TitleFormatter
A title formatter service for MediaWiki.
Definition: TitleFormatter.php:35
NS_CATEGORY
const NS_CATEGORY
Definition: Defines.php:78
NamespaceInfo
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Definition: NamespaceInfo.php:35
LinkCache\addGoodLinkObjFromRow
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Same as above with better interface.
Definition: LinkCache.php:315
NS_FILE
const NS_FILE
Definition: Defines.php:70
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
LinkCache\getGoodLinkRowInternal
getGoodLinkRowInternal(?TitleValue $link, callable $fetchCallback=null, int $queryFlags=IDBAccessObject::READ_NORMAL)
Definition: LinkCache.php:436
Page\PageIdentity\canExist
canExist()
Checks whether this PageIdentity represents a "proper" page, meaning that it could exist as an editab...
LinkCache\$wanCache
WANObjectCache $wanCache
Definition: LinkCache.php:47
LinkCache\addBadLinkObj
addBadLinkObj( $page)
Definition: LinkCache.php:341
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
LinkCache\getGoodLinkID
getGoodLinkID( $page)
Returns the ID of the given page, if information about this page has been cached.
Definition: LinkCache.php:184
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40