MediaWiki  master
LinkTargetStore.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Linker;
22 
23 use BagOStuff;
24 use InvalidArgumentException;
25 use RuntimeException;
26 use stdClass;
27 use TitleValue;
28 use WANObjectCache;
31 
38 
40  private $loadBalancer;
41 
43  private $localCache;
44 
46  private $wanObjectCache;
47 
49  private $byIdCache;
50 
52  private $byTitleCache;
53 
59  public function __construct(
62  WANObjectCache $WanObjectCache
63  ) {
64  $this->loadBalancer = $loadBalancer;
66  $this->wanObjectCache = $WanObjectCache;
67  $this->byIdCache = [];
68  $this->byTitleCache = [];
69  }
70 
74  public function newLinkTargetFromRow( stdClass $row ): LinkTarget {
75  $ltId = (int)$row->lt_id;
76  if ( $ltId === 0 ) {
77  throw new InvalidArgumentException(
78  "LinkTarget ID is 0 for {$row->lt_title} (ns: {$row->lt_namespace})"
79  );
80  }
81 
82  $titlevalue = new TitleValue( (int)$row->lt_namespace, $row->lt_title );
83  $this->addToClassCache( $ltId, $titlevalue );
84  return $titlevalue;
85  }
86 
93  public function getLinkTargetById( int $linkTargetId ): ?LinkTarget {
94  if ( !$linkTargetId ) {
95  return null;
96  }
97 
98  if ( isset( $this->byIdCache[$linkTargetId] ) ) {
99  return $this->byIdCache[$linkTargetId];
100  }
101 
102  $value = $this->loadBalancer->getConnectionRef( DB_REPLICA )->newSelectQueryBuilder()
103  ->caller( __METHOD__ )
104  ->table( 'linktarget' )
105  ->conds( [ 'lt_id' => $linkTargetId ] )
106  ->fields( [ 'lt_id', 'lt_namespace', 'lt_title' ] )
107  ->fetchRow();
108  if ( !$value ) {
109  return null;
110  }
111 
112  // TODO: Use local and wan cache when writing read new code.
113  $linkTarget = $this->newLinkTargetFromRow( $value );
114  $this->addToClassCache( $linkTargetId, $linkTarget );
115  return $linkTarget;
116  }
117 
124  public function getLinkTargetId( LinkTarget $linkTarget ): ?int {
125  // allow cache to be used, because if it is in the cache, it already has a linktarget id
126  $existingLinktargetId = $this->getLinkTargetIdFromCache( $linkTarget );
127  if ( $existingLinktargetId ) {
128  return $existingLinktargetId;
129  }
130  return null;
131  }
132 
146  public function acquireLinkTargetId( LinkTarget $linkTarget, IDatabase $dbw ): int {
147  // allow cache to be used, because if it is in the cache, it already has a linktarget id
148  $existingLinktargetId = $this->getLinkTargetIdFromCache( $linkTarget );
149  if ( $existingLinktargetId ) {
150  return $existingLinktargetId;
151  }
152 
153  // Checking primary when it doesn't exist in replica is not that useful but given
154  // the fact that failed inserts waste an auto_increment id better to avoid that.
155  $linkTargetId = $this->fetchIdFromDbPrimary( $linkTarget, [] );
156  if ( $linkTargetId ) {
157  $this->addToClassCache( $linkTargetId, $linkTarget );
158  return $linkTargetId;
159  }
160 
161  $ns = $linkTarget->getNamespace();
162  $title = $linkTarget->getDBkey();
163 
164  $dbw->insert(
165  'linktarget',
166  [
167  'lt_namespace' => $ns,
168  'lt_title' => $title,
169  ],
170  __METHOD__,
171  [ 'IGNORE' ]
172  );
173 
174  if ( $dbw->affectedRows() ) {
175  $linkTargetId = $dbw->insertId();
176  } else {
177  // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
178  $linkTargetId = $this->fetchIdFromDbPrimary(
179  $linkTarget,
180  [ 'LOCK IN SHARE MODE' ]
181  );
182  if ( !$linkTargetId ) {
183  throw new RuntimeException(
184  "Failed to create link target ID for " .
185  "lt_namespace={$ns} lt_title=\"{$title}\""
186  );
187  }
188  }
189  $this->addToClassCache( $linkTargetId, $linkTarget );
190 
191  return $linkTargetId;
192  }
193 
201  private function fetchIdFromDbPrimary(
202  LinkTarget $linkTarget,
203  array $queryOptions = []
204  ): ?int {
205  $row = $this->loadBalancer->getConnectionRef( DB_PRIMARY )->selectRow(
206  'linktarget',
207  [ 'lt_id', 'lt_namespace', 'lt_title' ],
208  [
209  'lt_namespace' => $linkTarget->getNamespace(),
210  'lt_title' => $linkTarget->getDBkey()
211  ],
212  __METHOD__,
213  $queryOptions
214  );
215 
216  if ( !$row || !$row->lt_id ) {
217  return null;
218  }
219  $this->addToClassCache( (int)$row->lt_id, $linkTarget );
220 
221  return (int)$row->lt_id;
222  }
223 
224  private function addToClassCache( int $id, LinkTarget $linkTarget ) {
225  $this->byIdCache[$id] = $linkTarget;
226  $this->byTitleCache[(string)$linkTarget] = $id;
227  }
228 
229  /*
230  * @internal use by tests only
231  */
232  public function clearClassCache() {
233  $this->byIdCache = [];
234  $this->byTitleCache = [];
235  }
236 
237  private function getLinkTargetIdFromCache( LinkTarget $linkTarget ) {
238  $linkTargetString = (string)$linkTarget;
239  if ( isset( $this->byTitleCache[$linkTargetString] ) ) {
240  return $this->byTitleCache[$linkTargetString];
241  }
242  $fname = __METHOD__;
243  $res = $this->localCache->getWithSetCallback(
244  $this->localCache->makeKey(
245  'linktargetstore-id',
246  $linkTargetString
247  ),
248  $this->localCache::TTL_HOUR,
249  function () use ( $linkTarget, $fname ) {
250  return $this->wanObjectCache->getWithSetCallback(
251  $this->wanObjectCache->makeKey(
252  'linktargetstore-id',
253  (string)$linkTarget
254  ),
255  WANObjectCache::TTL_DAY,
256  function () use ( $linkTarget, $fname ) {
257  $row = $this->loadBalancer->getConnectionRef( DB_REPLICA )->selectRow(
258  'linktarget',
259  [ 'lt_id', 'lt_namespace', 'lt_title' ],
260  [
261  'lt_namespace' => $linkTarget->getNamespace(),
262  'lt_title' => $linkTarget->getDBkey()
263  ],
264  $fname
265  );
266 
267  if ( !$row || !$row->lt_id ) {
268  // Don't store in cache
269  return false;
270  }
271 
272  return (int)$row->lt_id;
273  }
274  );
275  }
276  );
277 
278  if ( $res ) {
279  $this->addToClassCache( $res, $linkTarget );
280  }
281 
282  return $res;
283  }
284 
285 }
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition: WebStart.php:82
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:87
Service for retrieving and storing link targets.
getLinkTargetId(LinkTarget $linkTarget)
Return link target id if exists.
acquireLinkTargetId(LinkTarget $linkTarget, IDatabase $dbw)
Attempt to assign a link target ID to the given $linkTarget.
newLinkTargetFromRow(stdClass $row)
Instantiate a new LinkTarget object based on a $row from the linktarget table.Use this method when a ...
getLinkTargetById(int $linkTargetId)
Find a link target by $id.
array< int, $byIdCache;private array< string, $byTitleCache;public function __construct(ILoadBalancer $loadBalancer, BagOStuff $localCache, WANObjectCache $WanObjectCache) { $this->loadBalancer=$loadBalancer;$this-> localCache
LinkTarget>
addToClassCache(int $id, LinkTarget $linkTarget)
fetchIdFromDbPrimary(LinkTarget $linkTarget, array $queryOptions=[])
Find lt_id of the given $linkTarget.
getLinkTargetIdFromCache(LinkTarget $linkTarget)
__construct(IContextSource $context=null)
Definition: MediaWiki.php:61
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
Multi-datacenter aware caching interface.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part of the link target, in canonical database form.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:39
affectedRows()
Get the number of rows affected by the last write query.
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert row(s) into a table, in the provided order.
insertId()
Get the inserted value of an auto-increment row.
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition: defines.php:26
const DB_PRIMARY
Definition: defines.php:28