MediaWiki  master
LinkCache.php
Go to the documentation of this file.
1 <?php
28 use Psr\Log\LoggerAwareInterface;
29 use Psr\Log\LoggerInterface;
30 use Psr\Log\NullLogger;
34 
40 class LinkCache implements LoggerAwareInterface {
42  private $goodLinks;
44  private $badLinks;
46  private $wanCache;
47 
49  private $mForUpdate = false;
50 
52  private $titleFormatter;
53 
55  private $nsInfo;
56 
58  private $loadBalancer;
59 
61  private $logger;
62 
67  private const MAX_SIZE = 10000;
68 
75  public function __construct(
78  NamespaceInfo $nsInfo = null,
80  ) {
81  if ( !$nsInfo ) {
82  wfDeprecated( __METHOD__ . ' with no NamespaceInfo argument', '1.34' );
83  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
84  }
85  $this->goodLinks = new MapCacheLRU( self::MAX_SIZE );
86  $this->badLinks = new MapCacheLRU( self::MAX_SIZE );
87  $this->wanCache = $cache;
88  $this->titleFormatter = $titleFormatter;
89  $this->nsInfo = $nsInfo;
90  $this->loadBalancer = $loadBalancer;
91  $this->logger = new NullLogger();
92  }
93 
97  public function setLogger( LoggerInterface $logger ) {
98  $this->logger = $logger;
99  }
100 
112  public function forUpdate( $update = null ) {
113  wfDeprecated( __METHOD__, '1.34' ); // hard deprecated since 1.37
114  return wfSetVar( $this->mForUpdate, $update );
115  }
116 
123  private function getCacheKey( $page, $passThrough = false ) {
124  if ( is_string( $page ) ) {
125  if ( $passThrough ) {
126  return $page;
127  } else {
128  throw new InvalidArgumentException( 'They key may not be given as a string here' );
129  }
130  }
131 
132  if ( is_array( $page ) ) {
133  $namespace = $page['page_namespace'];
134  $dbkey = $page['page_title'];
135  return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
136  }
137 
138  if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
139  // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
140  $this->logger->info(
141  'cross-wiki page reference',
142  [
143  'page-wiki' => $page->getWikiId(),
144  'page-reference' => $this->titleFormatter->getFullText( $page )
145  ]
146  );
147  return null;
148  }
149 
150  if ( $page instanceof PageIdentity && !$page->canExist() ) {
151  // Non-proper page, perhaps a special page or interwiki link or relative section link.
152  $this->logger->warning(
153  'non-proper page reference: {page-reference}',
154  [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
155  );
156  return null;
157  }
158 
159  if ( $page instanceof LinkTarget
160  && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
161  ) {
162  // Interwiki link or relative section link. These do not have a page ID, so they
163  // can neither be "good" nor "bad" in the sense of this class.
164  $this->logger->warning(
165  'link to non-proper page: {page-link}',
166  [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
167  );
168  return null;
169  }
170 
171  return $this->titleFormatter->getPrefixedDBkey( $page );
172  }
173 
183  public function getGoodLinkID( $page ) {
184  $key = $this->getCacheKey( $page, true );
185 
186  if ( $key === null ) {
187  return 0;
188  }
189 
190  [ $row ] = $this->goodLinks->get( $key );
191 
192  return $row ? (int)$row->page_id : 0;
193  }
194 
207  public function getGoodLinkFieldObj( $page, string $field ) {
208  $key = $this->getCacheKey( $page );
209  if ( $key === null ) {
210  return null;
211  }
212 
213  if ( $this->isBadLink( $key ) ) {
214  return null;
215  }
216 
217  [ $row ] = $this->goodLinks->get( $key );
218 
219  if ( !$row ) {
220  return null;
221  }
222 
223  switch ( $field ) {
224  case 'id':
225  return intval( $row->page_id );
226  case 'length':
227  return intval( $row->page_len );
228  case 'redirect':
229  return intval( $row->page_is_redirect );
230  case 'revision':
231  return intval( $row->page_latest );
232  case 'model':
233  return !empty( $row->page_content_model )
234  ? strval( $row->page_content_model )
235  : null;
236  case 'lang':
237  return !empty( $row->page_lang )
238  ? strval( $row->page_lang )
239  : null;
240  case 'restrictions':
241  return !empty( $row->page_restrictions )
242  ? strval( $row->page_restrictions )
243  : null;
244  default:
245  throw new InvalidArgumentException( "Unknown field: $field" );
246  }
247  }
248 
258  public function isBadLink( $page ) {
259  $key = $this->getCacheKey( $page, true );
260  if ( $key === null ) {
261  return false;
262  }
263 
264  return $this->badLinks->has( $key );
265  }
266 
282  public function addGoodLinkObj( $id, $page, $len = -1, $redir = null,
283  $revision = 0, $model = null, $lang = null
284  ) {
285  $this->addGoodLinkObjFromRow( $page, (object)[
286  'page_id' => (int)$id,
287  'page_namespace' => $page->getNamespace(),
288  'page_title' => $page->getDBkey(),
289  'page_len' => (int)$len,
290  'page_is_redirect' => (int)$redir,
291  'page_latest' => (int)$revision,
292  'page_content_model' => $model ? (string)$model : null,
293  'page_lang' => $lang ? (string)$lang : null,
294  'page_restrictions' => null,
295  'page_is_new' => 0,
296  'page_touched' => '',
297  ] );
298  }
299 
313  public function addGoodLinkObjFromRow(
314  $page,
315  stdClass $row,
316  int $queryFlags = IDBAccessObject::READ_NORMAL
317  ) {
318  foreach ( self::getSelectFields() as $field ) {
319  if ( !property_exists( $row, $field ) ) {
320  throw new InvalidArgumentException( "Missing field: $field" );
321  }
322  }
323 
324  $key = $this->getCacheKey( $page );
325  if ( $key === null ) {
326  return;
327  }
328 
329  $this->goodLinks->set( $key, [ $row, $queryFlags ] );
330  $this->badLinks->clear( $key );
331  }
332 
339  public function addBadLinkObj( $page ) {
340  $key = $this->getCacheKey( $page );
341  if ( $key !== null && !$this->isBadLink( $key ) ) {
342  $this->badLinks->set( $key, 1 );
343  $this->goodLinks->clear( $key );
344  }
345  }
346 
353  public function clearBadLink( $page ) {
354  $key = $this->getCacheKey( $page, true );
355 
356  if ( $key !== null ) {
357  $this->badLinks->clear( $key );
358  }
359  }
360 
367  public function clearLink( $page ) {
368  $key = $this->getCacheKey( $page );
369 
370  if ( $key !== null ) {
371  $this->badLinks->clear( $key );
372  $this->goodLinks->clear( $key );
373  }
374  }
375 
382  public static function getSelectFields() {
383  global $wgPageLanguageUseDB;
384 
385  $fields = [
386  'page_id',
387  'page_len',
388  'page_is_redirect',
389  'page_latest',
390  'page_restrictions',
391  'page_content_model',
392  ];
393 
394  if ( $wgPageLanguageUseDB ) {
395  $fields[] = 'page_lang';
396  }
397 
398  return $fields;
399  }
400 
415  public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
416  $row = $this->getGoodLinkRow(
417  $page->getNamespace(),
418  $page->getDBkey(),
419  [ $this, 'fetchPageRow' ],
420  $queryFlags
421  );
422 
423  return $row ? (int)$row->page_id : 0;
424  }
425 
440  public function getGoodLinkRow(
441  int $ns,
442  string $dbkey,
443  callable $fetchCallback = null,
444  int $queryFlags = IDBAccessObject::READ_NORMAL
445  ): ?stdClass {
446  $link = TitleValue::tryNew( $ns, $dbkey );
447  $key = $link ? $this->getCacheKey( $link ) : null;
448  if ( $key === null ) {
449  return null;
450  }
451 
452  if ( $this->mForUpdate ) {
453  $queryFlags |= IDBAccessObject::READ_LATEST;
454  }
455  $forUpdate = $queryFlags & IDBAccessObject::READ_LATEST;
456 
457  if ( !$forUpdate && $this->isBadLink( $key ) ) {
458  return null;
459  }
460 
461  [ $row, $rowFlags ] = $this->goodLinks->get( $key );
462  if ( $row && $rowFlags >= $queryFlags ) {
463  return $row;
464  }
465 
466  if ( !$fetchCallback ) {
467  return null;
468  }
469 
470  if ( $this->usePersistentCache( $ns ) && !$forUpdate ) {
471  // Some pages are often transcluded heavily, so use persistent caching
472  $wanCacheKey = $this->wanCache->makeKey( 'page', $ns, sha1( $dbkey ) );
473 
474  $row = $this->wanCache->getWithSetCallback(
475  $wanCacheKey,
476  WANObjectCache::TTL_DAY,
477  function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
478  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
479  $setOpts += Database::getCacheSetOptions( $dbr );
480 
481  $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
482  $mtime = $row ? wfTimestamp( TS_UNIX, $row->page_touched ) : false;
483  $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
484 
485  return $row;
486  }
487  );
488  } else {
489  // No persistent caching needed, but we can still use the callback.
490  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
491  $dbr = $this->loadBalancer->getConnectionRef( $mode );
492  $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
493  }
494 
495  if ( $row ) {
496  $this->addGoodLinkObjFromRow( $link, $row );
497  } else {
498  $this->addBadLinkObj( $link );
499  }
500 
501  return $row ?: null;
502  }
503 
511  public function getMutableCacheKeys( WANObjectCache $cache, $page ) {
512  $key = $this->getCacheKey( $page );
513  // if no key can be derived, the page isn't cacheable
514  if ( $key === null ) {
515  return [];
516  }
517 
518  if ( $this->usePersistentCache( $page ) ) {
519  return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ];
520  }
521 
522  return [];
523  }
524 
530  private function usePersistentCache( $pageOrNamespace ) {
531  $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
532  if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
533  return true;
534  }
535  // Focus on transcluded pages more than the main content
536  if ( $this->nsInfo->isContent( $ns ) ) {
537  return false;
538  }
539  // Non-talk extension namespaces (e.g. NS_MODULE)
540  return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
541  }
542 
551  private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
552  $fields = self::getSelectFields();
553  if ( $this->usePersistentCache( $ns ) ) {
554  $fields[] = 'page_touched';
555  }
556 
557  return $db->selectRow(
558  'page',
559  $fields,
560  [ 'page_namespace' => $ns, 'page_title' => $dbkey ],
561  __METHOD__,
562  $options
563  );
564  }
565 
573  public function invalidateTitle( $page ) {
574  if ( $this->usePersistentCache( $page ) ) {
576  $cache->delete(
577  $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) )
578  );
579  }
580 
581  $this->clearLink( $page );
582  }
583 
587  public function clear() {
588  $this->goodLinks->clear();
589  $this->badLinks->clear();
590  }
591 
592 }
LinkCache\__construct
__construct(TitleFormatter $titleFormatter, WANObjectCache $cache, NamespaceInfo $nsInfo=null, ILoadBalancer $loadBalancer=null)
Definition: LinkCache.php:75
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:40
Wikimedia\Rdbms\Database
Relational database abstraction object.
Definition: Database.php:52
LinkCache\clearBadLink
clearBadLink( $page)
Definition: LinkCache.php:353
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:415
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
LinkCache\$goodLinks
MapCacheLRU $goodLinks
Definition: LinkCache.php:42
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:193
$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:1515
LinkCache\setLogger
setLogger(LoggerInterface $logger)
Definition: LinkCache.php:97
LinkCache\$logger
LoggerInterface $logger
Definition: LinkCache.php:61
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1668
LinkCache\invalidateTitle
invalidateTitle( $page)
Purge the persistent link cache for a title.
Definition: LinkCache.php:573
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:112
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:382
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:123
LinkCache\$badLinks
MapCacheLRU $badLinks
Definition: LinkCache.php:44
$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:258
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
TitleValue\tryNew
static tryNew( $namespace, $title, $fragment='', $interwiki='')
Constructs a TitleValue, or returns null if the parameters are not valid.
Definition: TitleValue.php:94
LinkCache\$titleFormatter
TitleFormatter $titleFormatter
Definition: LinkCache.php:52
LinkCache\getMutableCacheKeys
getMutableCacheKeys(WANObjectCache $cache, $page)
Definition: LinkCache.php:511
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:207
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
LinkCache\MAX_SIZE
const MAX_SIZE
How many Titles to store.
Definition: LinkCache.php:67
LinkCache\clearLink
clearLink( $page)
Definition: LinkCache.php:367
Page\PageReference\getWikiId
getWikiId()
Get the ID of the wiki this page belongs to.
$wgPageLanguageUseDB
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
Definition: DefaultSettings.php:2552
LinkCache\$nsInfo
NamespaceInfo $nsInfo
Definition: LinkCache.php:55
LinkCache\$loadBalancer
ILoadBalancer null $loadBalancer
Definition: LinkCache.php:58
LinkCache\$mForUpdate
bool $mForUpdate
Definition: LinkCache.php:49
LinkCache\fetchPageRow
fetchPageRow(IDatabase $db, int $ns, string $dbkey, $options=[])
Definition: LinkCache.php:551
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:530
WANObjectCache
Multi-datacenter aware caching interface.
Definition: WANObjectCache.php:128
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:440
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:282
$cache
$cache
Definition: mcc.php:33
LinkCache\clear
clear()
Clears cache.
Definition: LinkCache.php:587
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:313
NS_FILE
const NS_FILE
Definition: Defines.php:70
MediaWiki\Linker\LinkTarget
Definition: LinkTarget.php:26
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:46
LinkCache\addBadLinkObj
addBadLinkObj( $page)
Definition: LinkCache.php:339
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:183