MediaWiki REL1_37
LinkCache.php
Go to the documentation of this file.
1<?php
29use Psr\Log\LoggerAwareInterface;
30use Psr\Log\LoggerInterface;
31use Psr\Log\NullLogger;
35
41class LinkCache implements LoggerAwareInterface {
43 private $goodLinks;
45 private $badLinks;
47 private $wanCache;
48
50 private $mForUpdate = false;
51
54
56 private $nsInfo;
57
60
62 private $logger;
63
68 private const MAX_SIZE = 10000;
69
76 public function __construct(
77 TitleFormatter $titleFormatter,
79 NamespaceInfo $nsInfo = null,
80 ILoadBalancer $loadBalancer = 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
109 public static function singleton() {
110 wfDeprecated( __METHOD__, '1.28' );
111 return MediaWikiServices::getInstance()->getLinkCache();
112 }
113
125 public function forUpdate( $update = null ) {
126 wfDeprecated( __METHOD__, '1.34' ); // hard deprecated since 1.37
127 return wfSetVar( $this->mForUpdate, $update );
128 }
129
136 private function getCacheKey( $page, $passThrough = false ) {
137 if ( is_string( $page ) ) {
138 if ( $passThrough ) {
139 return $page;
140 } else {
141 throw new InvalidArgumentException( 'They key may not be given as a string here' );
142 }
143 }
144
145 if ( is_array( $page ) ) {
146 $namespace = $page['page_namespace'];
147 $dbkey = $page['page_title'];
148 return strtr( $this->titleFormatter->formatTitle( $namespace, $dbkey ), ' ', '_' );
149 }
150
151 if ( $page instanceof PageReference && $page->getWikiId() !== PageReference::LOCAL ) {
152 // No cross-wiki support yet. Perhaps LinkCache can become wiki-aware in the future.
153 $this->logger->info(
154 'cross-wiki page reference',
155 [
156 'page-wiki' => $page->getWikiId(),
157 'page-reference' => $this->titleFormatter->getFullText( $page )
158 ]
159 );
160 return null;
161 }
162
163 if ( $page instanceof PageIdentity && !$page->canExist() ) {
164 // Non-proper page, perhaps a special page or interwiki link or relative section link.
165 $this->logger->warning(
166 'non-proper page reference: {page-reference}',
167 [ 'page-reference' => $this->titleFormatter->getFullText( $page ) ]
168 );
169 return null;
170 }
171
172 if ( $page instanceof LinkTarget
173 && ( $page->isExternal() || $page->getText() === '' || $page->getNamespace() < 0 )
174 ) {
175 // Interwiki link or relative section link. These do not have a page ID, so they
176 // can neither be "good" nor "bad" in the sense of this class.
177 $this->logger->warning(
178 'link to non-proper page: {page-link}',
179 [ 'page-link' => $this->titleFormatter->getFullText( $page ) ]
180 );
181 return null;
182 }
183
184 return $this->titleFormatter->getPrefixedDBkey( $page );
185 }
186
196 public function getGoodLinkID( $page ) {
197 $key = $this->getCacheKey( $page, true );
198
199 if ( $key === null ) {
200 return 0;
201 }
202
203 [ $row ] = $this->goodLinks->get( $key );
204
205 return $row ? (int)$row->page_id : 0;
206 }
207
220 public function getGoodLinkFieldObj( $page, string $field ) {
221 $key = $this->getCacheKey( $page );
222 if ( $key === null ) {
223 return null;
224 }
225
226 if ( $this->isBadLink( $key ) ) {
227 return null;
228 }
229
230 [ $row ] = $this->goodLinks->get( $key );
231
232 if ( !$row ) {
233 return null;
234 }
235
236 switch ( $field ) {
237 case 'id':
238 return intval( $row->page_id );
239 case 'length':
240 return intval( $row->page_len );
241 case 'redirect':
242 return intval( $row->page_is_redirect );
243 case 'revision':
244 return intval( $row->page_latest );
245 case 'model':
246 return !empty( $row->page_content_model )
247 ? strval( $row->page_content_model )
248 : null;
249 case 'lang':
250 return !empty( $row->page_lang )
251 ? strval( $row->page_lang )
252 : null;
253 case 'restrictions':
254 return !empty( $row->page_restrictions )
255 ? strval( $row->page_restrictions )
256 : null;
257 default:
258 throw new InvalidArgumentException( "Unknown field: $field" );
259 }
260 }
261
271 public function isBadLink( $page ) {
272 $key = $this->getCacheKey( $page, true );
273 if ( $key === null ) {
274 return false;
275 }
276
277 return $this->badLinks->has( $key );
278 }
279
295 public function addGoodLinkObj( $id, $page, $len = -1, $redir = null,
296 $revision = 0, $model = null, $lang = null
297 ) {
298 $this->addGoodLinkObjFromRow( $page, (object)[
299 'page_id' => (int)$id,
300 'page_namespace' => $page->getNamespace(),
301 'page_title' => $page->getDBkey(),
302 'page_len' => (int)$len,
303 'page_is_redirect' => (int)$redir,
304 'page_latest' => (int)$revision,
305 'page_content_model' => $model ? (string)$model : null,
306 'page_lang' => $lang ? (string)$lang : null,
307 'page_restrictions' => null,
308 'page_is_new' => 0,
309 'page_touched' => '',
310 ] );
311 }
312
326 public function addGoodLinkObjFromRow(
327 $page,
328 stdClass $row,
329 int $queryFlags = IDBAccessObject::READ_NORMAL
330 ) {
331 foreach ( self::getSelectFields() as $field ) {
332 if ( !property_exists( $row, $field ) ) {
333 throw new InvalidArgumentException( "Missing field: $field" );
334 }
335 }
336
337 $key = $this->getCacheKey( $page );
338 if ( $key === null ) {
339 return;
340 }
341
342 $this->goodLinks->set( $key, [ $row, $queryFlags ] );
343 $this->badLinks->clear( $key );
344 }
345
352 public function addBadLinkObj( $page ) {
353 $key = $this->getCacheKey( $page );
354 if ( $key !== null && !$this->isBadLink( $key ) ) {
355 $this->badLinks->set( $key, 1 );
356 $this->goodLinks->clear( $key );
357 }
358 }
359
366 public function clearBadLink( $page ) {
367 $key = $this->getCacheKey( $page, true );
368
369 if ( $key !== null ) {
370 $this->badLinks->clear( $key );
371 }
372 }
373
380 public function clearLink( $page ) {
381 $key = $this->getCacheKey( $page );
382
383 if ( $key !== null ) {
384 $this->badLinks->clear( $key );
385 $this->goodLinks->clear( $key );
386 }
387 }
388
395 public static function getSelectFields() {
397
398 $fields = array_merge(
399 PageStoreRecord::REQUIRED_FIELDS,
400 [
401 'page_len',
402 'page_restrictions',
403 'page_content_model',
404 ]
405 );
406
407 if ( $wgPageLanguageUseDB ) {
408 $fields[] = 'page_lang';
409 }
410
411 return $fields;
412 }
413
428 public function addLinkObj( $page, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
429 $row = $this->getGoodLinkRow(
430 $page->getNamespace(),
431 $page->getDBkey(),
432 [ $this, 'fetchPageRow' ],
433 $queryFlags
434 );
435
436 return $row ? (int)$row->page_id : 0;
437 }
438
447 private function getGoodLinkRowInternal(
448 ?TitleValue $link,
449 callable $fetchCallback = null,
450 int $queryFlags = IDBAccessObject::READ_NORMAL
451 ): array {
452 $key = $link ? $this->getCacheKey( $link ) : null;
453 if ( $key === null ) {
454 return [ false, false ];
455 }
456
457 $ns = $link->getNamespace();
458 $dbkey = $link->getDBkey();
459 $callerShouldAddGoodLink = false;
460
461 if ( $this->mForUpdate ) {
462 $queryFlags |= IDBAccessObject::READ_LATEST;
463 }
464 $forUpdate = $queryFlags & IDBAccessObject::READ_LATEST;
465
466 if ( !$forUpdate && $this->isBadLink( $key ) ) {
467 return [ $callerShouldAddGoodLink, false ];
468 }
469
470 [ $row, $rowFlags ] = $this->goodLinks->get( $key );
471 if ( $row && $rowFlags >= $queryFlags ) {
472 return [ $callerShouldAddGoodLink, $row ];
473 }
474
475 if ( !$fetchCallback ) {
476 return [ $callerShouldAddGoodLink, false ];
477 }
478
479 $callerShouldAddGoodLink = true;
480 if ( $this->usePersistentCache( $ns ) && !$forUpdate ) {
481 // Some pages are often transcluded heavily, so use persistent caching
482 $wanCacheKey = $this->wanCache->makeKey( 'page', $ns, sha1( $dbkey ) );
483
484 $row = $this->wanCache->getWithSetCallback(
485 $wanCacheKey,
486 WANObjectCache::TTL_DAY,
487 function ( $curValue, &$ttl, array &$setOpts ) use ( $fetchCallback, $ns, $dbkey ) {
488 $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
489 $setOpts += Database::getCacheSetOptions( $dbr );
490
491 $row = $fetchCallback( $dbr, $ns, $dbkey, [] );
492 $mtime = $row ? wfTimestamp( TS_UNIX, $row->page_touched ) : false;
493 $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl );
494
495 return $row;
496 }
497 );
498 } else {
499 // No persistent caching needed, but we can still use the callback.
500 [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
501 $dbr = $this->loadBalancer->getConnectionRef( $mode );
502 $row = $fetchCallback( $dbr, $ns, $dbkey, $options );
503 }
504
505 return [ $callerShouldAddGoodLink, $row ];
506 }
507
522 public function getGoodLinkRow(
523 int $ns,
524 string $dbkey,
525 callable $fetchCallback = null,
526 int $queryFlags = IDBAccessObject::READ_NORMAL
527 ): ?stdClass {
528 $link = TitleValue::tryNew( $ns, $dbkey );
529 if ( $link === null ) {
530 return null;
531 }
532 [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
533 $link,
534 $fetchCallback,
535 $queryFlags
536 );
537
538 if ( $row ) {
539 if ( $shouldAddGoodLink ) {
540 try {
541 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
542 } catch ( InvalidArgumentException $e ) {
543 // a field is missing from $row; maybe we used a cache?; invalidate it and try again
544 $this->invalidateTitle( $link );
545 [ $shouldAddGoodLink, $row ] = $this->getGoodLinkRowInternal(
546 $link,
547 $fetchCallback,
548 $queryFlags
549 );
550 $this->addGoodLinkObjFromRow( $link, $row, $queryFlags );
551 }
552 }
553 } else {
554 $this->addBadLinkObj( $link );
555 }
556
557 return $row ?: null;
558 }
559
567 public function getMutableCacheKeys( WANObjectCache $cache, $page ) {
568 $key = $this->getCacheKey( $page );
569 // if no key can be derived, the page isn't cacheable
570 if ( $key === null ) {
571 return [];
572 }
573
574 if ( $this->usePersistentCache( $page ) ) {
575 return [ $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) ) ];
576 }
577
578 return [];
579 }
580
586 private function usePersistentCache( $pageOrNamespace ) {
587 $ns = is_int( $pageOrNamespace ) ? $pageOrNamespace : $pageOrNamespace->getNamespace();
588 if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
589 return true;
590 }
591 // Focus on transcluded pages more than the main content
592 if ( $this->nsInfo->isContent( $ns ) ) {
593 return false;
594 }
595 // Non-talk extension namespaces (e.g. NS_MODULE)
596 return ( $ns >= 100 && $this->nsInfo->isSubject( $ns ) );
597 }
598
607 private function fetchPageRow( IDatabase $db, int $ns, string $dbkey, $options = [] ) {
608 $fields = self::getSelectFields();
609 if ( $this->usePersistentCache( $ns ) ) {
610 $fields[] = 'page_touched';
611 }
612
613 return $db->selectRow(
614 'page',
615 $fields,
616 [ 'page_namespace' => $ns, 'page_title' => $dbkey ],
617 __METHOD__,
618 $options
619 );
620 }
621
629 public function invalidateTitle( $page ) {
630 if ( $this->usePersistentCache( $page ) ) {
631 $cache = $this->wanCache;
632 $cache->delete(
633 $cache->makeKey( 'page', $page->getNamespace(), sha1( $page->getDBkey() ) )
634 );
635 }
636
637 $this->clearLink( $page );
638 }
639
643 public function clear() {
644 $this->goodLinks->clear();
645 $this->badLinks->clear();
646 }
647
648}
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
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
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...
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:41
TitleFormatter $titleFormatter
Definition LinkCache.php:53
isBadLink( $page)
Returns true if the fact that this page does not exist had been added to the cache.
clear()
Clears cache.
getCacheKey( $page, $passThrough=false)
fetchPageRow(IDatabase $db, int $ns, string $dbkey, $options=[])
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.
NamespaceInfo $nsInfo
Definition LinkCache.php:56
clearLink( $page)
forUpdate( $update=null)
General accessor to get/set whether the primary DB should be used.
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Same as above with better interface.
MapCacheLRU $goodLinks
Definition LinkCache.php:43
clearBadLink( $page)
bool $mForUpdate
Definition LinkCache.php:50
usePersistentCache( $pageOrNamespace)
addGoodLinkObj( $id, $page, $len=-1, $redir=null, $revision=0, $model=null, $lang=null)
Add information about an existing page to the cache.
getGoodLinkID( $page)
Returns the ID of the given page, if information about this page has been cached.
setLogger(LoggerInterface $logger)
Definition LinkCache.php:98
addBadLinkObj( $page)
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).
LoggerInterface $logger
Definition LinkCache.php:62
WANObjectCache $wanCache
Definition LinkCache.php:47
getGoodLinkRowInternal(?TitleValue $link, callable $fetchCallback=null, int $queryFlags=IDBAccessObject::READ_NORMAL)
static singleton()
Get an instance of this class.
const MAX_SIZE
How many Titles to store.
Definition LinkCache.php:68
static getSelectFields()
Fields that LinkCache needs to select.
ILoadBalancer null $loadBalancer
Definition LinkCache.php:59
getGoodLinkFieldObj( $page, string $field)
Get a field of a page from the cache.
getMutableCacheKeys(WANObjectCache $cache, $page)
MapCacheLRU $badLinks
Definition LinkCache.php:45
__construct(TitleFormatter $titleFormatter, WANObjectCache $cache, NamespaceInfo $nsInfo=null, ILoadBalancer $loadBalancer=null)
Definition LinkCache.php:76
Handles a simple LRU key/value map with a maximum number of entries.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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()
Returns the title's DB key, as supplied to the constructor, without namespace prefix or fragment.
Multi-datacenter aware caching interface.
Relational database abstraction object.
Definition Database.php:52
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:38
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
Database cluster connection, tracking, load balancing, and transaction manager interface.
$cache
Definition mcc.php:33
if(!isset( $args[0])) $lang