MediaWiki master
LinkAlwaysKnownLookup.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Page;
4
5use LogicException;
12use Psr\Log\LoggerInterface;
13use RuntimeException;
15
17
18 private readonly MapCacheLRU $cache;
20 private int $individualLookupsInRequest = 0;
21 private const int MAX_INDIVIDUAL_LOOKUPS_PER_REQ = 50;
22
23 public function __construct(
24 private readonly HookRunner $hookRunner,
25 private readonly TitleFactory $titleFactory,
26 private readonly ShadowPageLoader $shadowPageLoader,
27 private readonly RepoGroup $repoGroup,
28 private readonly SpecialPageFactory $specialPageFactory,
29 private readonly LoggerInterface $logger
30 ) {
31 $this->cache = new MapCacheLRU( 100_000 );
32 }
33
46 private function computeIsAlwaysKnownUnbatchedParts( LinkTarget $link ): bool {
47 // sanity check
48 $cacheKey = CacheKeyHelper::getKeyForPage( $link );
49 if ( !$this->cache->has( $cacheKey ) || $this->cache->get( $cacheKey ) !== null ) {
50 throw new LogicException(
51 __METHOD__ . ' can be only called when cached value is null'
52 );
53 }
54
55 // TODO: Remove this hook once all callers are mitigated (T433161)
56 $this->hookRunner->onTitleIsAlwaysKnown(
57 $this->titleFactory->newFromLinkTarget( $link ),
58 $isKnown
59 );
60
61 if ( $isKnown === null ) {
62 // Even the second hook made no decision for us, we REALLY
63 // have to decide ourselves...
64 if ( $link->isExternal() ) {
65 // any interwiki link might be viewable, for all we know
66 $isKnown = true;
67 } elseif ( $this->shadowPageLoader->existsForLink( $link ) ) {
68 $isKnown = true;
69 } else {
70 $isKnown = match ( $link->getNamespace() ) {
71 // file exists, possibly in a foreign repo
72 // TODO: it might make sense to switch to RepoGroup::findFiles and
73 // batch this as well
74 NS_MEDIA, NS_FILE => (bool)$this->repoGroup->findFile( $link ),
75 // if the title is a valid special page, it exists
76 NS_SPECIAL => $this->specialPageFactory->exists( $link->getDBkey() ),
77 // self-link, possibly with fragment
78 NS_MAIN => $link->getDBkey() == '',
79 default => false,
80 };
81 }
82 }
83
84 // sanity check
85 if ( $isKnown === null ) {
86 throw new LogicException(
87 __METHOD__ . ' should have the final call; $isKnown === null should not be possible'
88 );
89 }
90
91 $this->cache->set( $cacheKey, $isKnown );
92 return $isKnown;
93 }
94
107 private function computeIsAlwaysKnownBatch( array $links ): void {
108 $isKnownArr = array_fill_keys( array_keys( $links ), null );
109 $this->hookRunner->onLinkTargetIsAlwaysKnownBatch( $links, $isKnownArr );
110
111 foreach ( $links as $i => $link ) {
112 $isKnown = $isKnownArr[$i] ?? null;
113
114 // NOTE: $isKnown can be null, which means "batched computation was executed, and
115 // result was not determined". Caller interprets that as "run
116 // computeIsAlwaysKnownUnbatchedParts()".
117 $this->cache->set(
119 $isKnown
120 );
121 }
122 }
123
127 public function preload( array $links ): void {
128 $uncachedLinks = [];
129 foreach ( $links as $link ) {
130 if ( !$this->cache->has( CacheKeyHelper::getKeyForPage( $link ) ) ) {
131 $uncachedLinks[] = $link;
132 }
133 }
134
135 // Batched lookups should be (relatively) cheap. Compute what can be computed
136 // on a preload. If the result cannot be determined in batched way,
137 // isAlwaysKnown will call computeIsAlwaysKnownUnbatchedParts() to determine
138 // the final result
139 // NOTE: computeIsAlwaysKnownBatch is responsible for writing back into the cache
140 if ( $uncachedLinks ) {
141 $this->computeIsAlwaysKnownBatch( $uncachedLinks );
142 }
143 }
144
145 public function isAlwaysKnown( LinkTarget $page ): bool {
146 $key = CacheKeyHelper::getKeyForPage( $page );
147 if ( !$this->cache->has( $key ) ) {
148 // The compute method writes back to the cache
149 $this->computeIsAlwaysKnownBatch( [ $page ] );
150
151 if ( ++$this->individualLookupsInRequest >= self::MAX_INDIVIDUAL_LOOKUPS_PER_REQ ) {
152 $this->logger->warning(
153 __METHOD__ . ' was used more than {limit} times (value: {value}), use batching',
154 [
155 'limit' => self::MAX_INDIVIDUAL_LOOKUPS_PER_REQ,
156 'value' => $this->individualLookupsInRequest,
157 'exception' => new RuntimeException,
158 ]
159 );
160 }
161 }
162
163 $cachedValue = $this->cache->get( $key );
164 if ( $cachedValue !== null ) {
165 return (bool)$cachedValue;
166 }
167
168 // Batched computation did not determine the final result
169 // Cache was written to by callee
170 return $this->computeIsAlwaysKnownUnbatchedParts( $page );
171 }
172}
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
const NS_MEDIA
Definition Defines.php:39
Prioritized list of file repositories.
Definition RepoGroup.php:30
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Helper class for mapping page value objects to a string key.
static getKeyForPage( $page)
Returns a stable key for identifying the given page in a cache.
__construct(private readonly HookRunner $hookRunner, private readonly TitleFactory $titleFactory, private readonly ShadowPageLoader $shadowPageLoader, private readonly RepoGroup $repoGroup, private readonly SpecialPageFactory $specialPageFactory, private readonly LoggerInterface $logger)
A service which loads shadow content, which is content that is displayed on a nonexistent page with a...
Factory for handling the special page list and generating SpecialPage objects.
Creates Title objects.
Store key-value entries in a size-limited in-memory LRU cache.
Represents the target of a wiki link.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
isExternal()
Whether this LinkTarget has an interwiki component.