MediaWiki REL1_39
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 $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.
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)
addGoodLinkObjFromRow( $page, stdClass $row, int $queryFlags=IDBAccessObject::READ_NORMAL)
Same as above with better interface.
clearBadLink( $page)
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:92
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).
static getSelectFields()
Fields that LinkCache needs to select.
getGoodLinkFieldObj( $page, string $field)
Get a field of a page from the cache.
getMutableCacheKeys(WANObjectCache $cache, $page)
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: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
if(!isset( $args[0])) $lang