MediaWiki REL1_37
ExternalStoreDB.php
Go to the documentation of this file.
1<?php
30use Wikimedia\ScopedCallback;
31
42 private $lbFactory;
43
49 public function __construct( array $params ) {
50 parent::__construct( $params );
51 if ( !isset( $params['lbFactory'] ) || !( $params['lbFactory'] instanceof LBFactory ) ) {
52 throw new InvalidArgumentException( "LBFactory required in 'lbFactory' field." );
53 }
54 $this->lbFactory = $params['lbFactory'];
55 }
56
65 public function fetchFromURL( $url ) {
66 list( $cluster, $id, $itemID ) = $this->parseURL( $url );
67 $ret = $this->fetchBlob( $cluster, $id, $itemID );
68
69 if ( $itemID !== false && $ret !== false ) {
70 return $ret->getItem( $itemID );
71 }
72
73 return $ret;
74 }
75
85 public function batchFetchFromURLs( array $urls ) {
86 $batched = $inverseUrlMap = [];
87 foreach ( $urls as $url ) {
88 list( $cluster, $id, $itemID ) = $this->parseURL( $url );
89 $batched[$cluster][$id][] = $itemID;
90 // false $itemID gets cast to int, but should be ok
91 // since we do === from the $itemID in $batched
92 $inverseUrlMap[$cluster][$id][$itemID] = $url;
93 }
94 $ret = [];
95 foreach ( $batched as $cluster => $batchByCluster ) {
96 $res = $this->batchFetchBlobs( $cluster, $batchByCluster );
98 foreach ( $res as $id => $blob ) {
99 foreach ( $batchByCluster[$id] as $itemID ) {
100 $url = $inverseUrlMap[$cluster][$id][$itemID];
101 if ( $itemID === false ) {
102 $ret[$url] = $blob;
103 } else {
104 $ret[$url] = $blob->getItem( $itemID );
105 }
106 }
107 }
108 }
109
110 return $ret;
111 }
112
116 public function store( $location, $data ) {
117 $dbw = $this->getPrimary( $location );
118 $dbw->insert(
119 $this->getTable( $dbw, $location ),
120 [ 'blob_text' => $data ],
121 __METHOD__
122 );
123 $id = $dbw->insertId();
124 if ( !$id ) {
125 throw new MWException( __METHOD__ . ': no insert ID' );
126 }
127
128 return "DB://$location/$id";
129 }
130
134 public function isReadOnly( $location ) {
135 if ( parent::isReadOnly( $location ) ) {
136 return true;
137 }
138
139 $lb = $this->getLoadBalancer( $location );
140 $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
141
142 return ( $lb->getReadOnlyReason( $domainId ) !== false );
143 }
144
151 private function getLoadBalancer( $cluster ) {
152 return $this->lbFactory->getExternalLB( $cluster );
153 }
154
162 public function getReplica( $cluster ) {
163 $lb = $this->getLoadBalancer( $cluster );
164
165 return $lb->getConnectionRef(
167 [],
168 $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
169 $lb::CONN_TRX_AUTOCOMMIT
170 );
171 }
172
180 public function getSlave( $cluster ) {
181 wfDeprecated( __METHOD__, '1.34' );
182 return $this->getReplica( $cluster );
183 }
184
192 public function getPrimary( $cluster ) {
193 $lb = $this->getLoadBalancer( $cluster );
194
195 return $lb->getMaintenanceConnectionRef(
197 [],
198 $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
199 $lb::CONN_TRX_AUTOCOMMIT
200 );
201 }
202
208 public function getMaster( $cluster ) {
209 wfDeprecated( __METHOD__, '1.37' );
210 return $this->getPrimary( $cluster );
211 }
212
217 private function getDomainId( array $server ) {
218 if ( $this->isDbDomainExplicit ) {
219 return $this->dbDomain; // explicit foreign domain
220 }
221
222 if ( isset( $server['dbname'] ) ) {
223 // T200471: for b/c, treat any "dbname" field as forcing which database to use.
224 // MediaWiki/LoadBalancer previously did not enforce any concept of a local DB
225 // domain, but rather assumed that the LB server configuration matched $wgDBname.
226 // This check is useful when the external storage DB for this cluster does not use
227 // the same name as the corresponding "main" DB(s) for wikis.
228 $domain = new DatabaseDomain(
229 $server['dbname'],
230 $server['schema'] ?? null,
231 $server['tablePrefix'] ?? ''
232 );
233
234 return $domain->getId();
235 }
236
237 return false; // local LB domain
238 }
239
247 public function getTable( $db, $cluster = null ) {
248 if ( $cluster !== null ) {
249 $lb = $this->getLoadBalancer( $cluster );
250 $info = $lb->getServerInfo( $lb->getWriterIndex() );
251 if ( isset( $info['blobs table'] ) ) {
252 return $info['blobs table'];
253 }
254 }
255
256 return $db->getLBInfo( 'blobs table' ) ?? 'blobs'; // b/c
257 }
258
266 public function initializeTable( $cluster ) {
267 global $IP;
268
269 static $supportedTypes = [ 'mysql', 'sqlite' ];
270
271 $dbw = $this->getPrimary( $cluster );
272 if ( !in_array( $dbw->getType(), $supportedTypes, true ) ) {
273 throw new DBUnexpectedError( $dbw, "RDBMS type '{$dbw->getType()}' not supported." );
274 }
275
276 $sqlFilePath = "$IP/maintenance/storage/blobs.sql";
277 $sql = file_get_contents( $sqlFilePath );
278 if ( $sql === false ) {
279 throw new RuntimeException( "Failed to read '$sqlFilePath'." );
280 }
281
282 $rawTable = $this->getTable( $dbw, $cluster ); // e.g. "blobs_cluster23"
283 $encTable = $dbw->tableName( $rawTable );
284 $dbw->query(
285 str_replace(
286 [ '/*$wgDBprefix*/blobs', '/*_*/blobs' ],
287 [ $encTable, $encTable ],
288 $sql
289 ),
290 __METHOD__,
291 $dbw::QUERY_IGNORE_DBO_TRX
292 );
293 }
294
304 private function fetchBlob( $cluster, $id, $itemID ) {
311 static $externalBlobCache = [];
312
313 $cacheID = ( $itemID === false ) ? "$cluster/$id" : "$cluster/$id/";
314 $cacheID = "$cacheID@{$this->dbDomain}";
315
316 if ( isset( $externalBlobCache[$cacheID] ) ) {
317 $this->logger->debug( __METHOD__ . ": cache hit on $cacheID" );
318
319 return $externalBlobCache[$cacheID];
320 }
321
322 $this->logger->debug( __METHOD__ . ": cache miss on $cacheID" );
323
324 $dbr = $this->getReplica( $cluster );
325 $ret = $dbr->selectField(
326 $this->getTable( $dbr, $cluster ),
327 'blob_text',
328 [ 'blob_id' => $id ],
329 __METHOD__
330 );
331 if ( $ret === false ) {
332 // Try the primary DB
333 $this->logger->warning( __METHOD__ . ": primary DB fallback on $cacheID" );
334 $scope = $this->lbFactory->getTransactionProfiler()->silenceForScope();
335 $dbw = $this->getPrimary( $cluster );
336 $ret = $dbw->selectField(
337 $this->getTable( $dbw, $cluster ),
338 'blob_text',
339 [ 'blob_id' => $id ],
340 __METHOD__
341 );
342 ScopedCallback::consume( $scope );
343 if ( $ret === false ) {
344 $this->logger->warning( __METHOD__ . ": primary DB failed to find $cacheID" );
345 }
346 }
347 if ( $itemID !== false && $ret !== false ) {
348 // Unserialise object; caller extracts item
349 $ret = unserialize( $ret );
350 }
351
352 $externalBlobCache = [ $cacheID => $ret ];
353
354 return $ret;
355 }
356
365 private function batchFetchBlobs( $cluster, array $ids ) {
366 $dbr = $this->getReplica( $cluster );
367 $res = $dbr->select(
368 $this->getTable( $dbr, $cluster ),
369 [ 'blob_id', 'blob_text' ],
370 [ 'blob_id' => array_keys( $ids ) ],
371 __METHOD__
372 );
373
374 $ret = [];
375 if ( $res !== false ) {
376 $this->mergeBatchResult( $ret, $ids, $res );
377 }
378 if ( $ids ) {
379 // Try the primary
380 $this->logger->info(
381 __METHOD__ . ": primary fallback on '$cluster' for: " .
382 implode( ',', array_keys( $ids ) )
383 );
384 $scope = $this->lbFactory->getTransactionProfiler()->silenceForScope();
385 $dbw = $this->getPrimary( $cluster );
386 $res = $dbw->select(
387 $this->getTable( $dbr, $cluster ),
388 [ 'blob_id', 'blob_text' ],
389 [ 'blob_id' => array_keys( $ids ) ],
390 __METHOD__ );
391 ScopedCallback::consume( $scope );
392 if ( $res === false ) {
393 $this->logger->error( __METHOD__ . ": primary failed on '$cluster'" );
394 } else {
395 $this->mergeBatchResult( $ret, $ids, $res );
396 }
397 }
398 if ( $ids ) {
399 $this->logger->error(
400 __METHOD__ . ": primary on '$cluster' failed locating items: " .
401 implode( ',', array_keys( $ids ) )
402 );
403 }
404
405 return $ret;
406 }
407
414 private function mergeBatchResult( array &$ret, array &$ids, $res ) {
415 foreach ( $res as $row ) {
416 $id = $row->blob_id;
417 $itemIDs = $ids[$id];
418 unset( $ids[$id] ); // to track if everything is found
419 if ( count( $itemIDs ) === 1 && reset( $itemIDs ) === false ) {
420 // single result stored per blob
421 $ret[$id] = $row->blob_text;
422 } else {
423 // multi result stored per blob
424 $ret[$id] = unserialize( $row->blob_text );
425 }
426 }
427 }
428
433 protected function parseURL( $url ) {
434 $path = explode( '/', $url );
435
436 return [
437 $path[2], // cluster
438 $path[3], // id
439 $path[4] ?? false // itemID
440 ];
441 }
442}
unserialize( $serialized)
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$IP
Definition WebStart.php:49
DB accessible external objects.
getSlave( $cluster)
Get a replica DB connection for the specified cluster.
getPrimary( $cluster)
Get a primary database connection for the specified cluster.
__construct(array $params)
batchFetchBlobs( $cluster, array $ids)
Fetch multiple blob items out of the database.
getReplica( $cluster)
Get a replica DB connection for the specified cluster.
mergeBatchResult(array &$ret, array &$ids, $res)
Helper function for self::batchFetchBlobs for merging primary/replica DB results.
getDomainId(array $server)
initializeTable( $cluster)
Create the appropriate blobs table on this cluster.
fetchBlob( $cluster, $id, $itemID)
Fetch a blob item out of the database; a cache of the last-loaded blob will be kept so that multiple ...
fetchFromURL( $url)
The provided URL is in the form of DB://cluster/id or DB://cluster/id/itemid for concatened storage.
getTable( $db, $cluster=null)
Get the 'blobs' table name for this database.
store( $location, $data)
Insert a data item into a given location.string|bool The URL of the stored data item,...
batchFetchFromURLs(array $urls)
Fetch data from given external store URLs.
isReadOnly( $location)
Check if a given location is read-only.bool Whether this location is read-only 1.31
getLoadBalancer( $cluster)
Get a LoadBalancer for the specified cluster.
Key/value blob storage for a particular storage medium type (e.g.
array $params
Usage context options for this instance.
string $dbDomain
Default database domain to store content under.
MediaWiki exception.
Helper class used for automatically marking an IDatabase connection as reusable (once it no longer ma...
Definition DBConnRef.php:29
Class to handle database/schema/prefix specifications for IDatabase.
An interface for generating database load balancers.
Definition LBFactory.php:42
Helper class to handle automatically marking connections as reusable (via RAII pattern) as well handl...
Base class for general text storage via the "object" flag in old_flags, or two-part external storage ...
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27