MediaWiki 1.40.4
LinkCache.php
Go to the documentation of this file.
1<?php
30use Psr\Log\LoggerAwareInterface;
31use Psr\Log\LoggerInterface;
32use Psr\Log\NullLogger;
36
42class 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.
clear()
Clears cache.
invalidateTitle( $page)
Purge the persistent link cache for a title.
addLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add a title to the link cache, return the page_id or zero if non-existent.
clearLink( $page)
Clear information about a page from the process cache.
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about an existing page to the process cache.
clearBadLink( $page)
Clear information about a page being missing from the process cache.
addGoodLinkObj( $id, $page, $len=-1, $redir=null, $revision=0, $model=null, $lang=null)
Add information about an existing page to the process cache.
addBadLinkObj( $page, int $queryFlags=IDBAccessObject::READ_NORMAL)
Add information about a missing page to the process cache.
getGoodLinkID( $page)
Get the ID of a page known to the process cache.
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).
static getSelectFields()
Fields that LinkCache needs to select.
getGoodLinkFieldObj( $page, string $field)
Get the field of a page known to the process cache.
Handles a simple LRU key/value map with a maximum number of entries.
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.
getDBkey()
Get the main part of the link target, in canonical database form.
getNamespace()
Get the namespace index.
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.
if(!isset( $args[0])) $lang