50 throw new InvalidArgumentException(
"LBFactory required in 'lbFactory' field." );
52 $this->lbFactory =
$params[
'lbFactory'];
66 list( $cluster, $id, $itemID ) = $this->
parseURL( $url );
67 $ret = $this->fetchBlob( $cluster, $id, $itemID );
69 if ( $itemID !==
false && $ret !==
false ) {
70 return $ret->getItem( $itemID );
87 $batched = $inverseUrlMap = [];
88 foreach ( $urls as $url ) {
89 list( $cluster, $id, $itemID ) = $this->
parseURL( $url );
90 $batched[$cluster][$id][] = $itemID;
93 $inverseUrlMap[$cluster][$id][$itemID] = $url;
96 foreach ( $batched as $cluster => $batchByCluster ) {
97 $res = $this->batchFetchBlobs( $cluster, $batchByCluster );
100 foreach ( $batchByCluster[$id] as $itemID ) {
101 $url = $inverseUrlMap[$cluster][$id][$itemID];
102 if ( $itemID ===
false ) {
105 $ret[$url] =
$blob->getItem( $itemID );
117 public function store( $location, $data ) {
121 [
'blob_text' => $data ],
124 $id = $dbw->insertId();
126 throw new MWException( __METHOD__ .
': no insert ID' );
129 return "DB://$location/$id";
136 if ( parent::isReadOnly( $location ) ) {
140 $lb = $this->getLoadBalancer( $location );
141 $domainId = $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) );
143 return ( $lb->getReadOnlyReason( $domainId ) !==
false );
152 private function getLoadBalancer( $cluster ) {
153 return $this->lbFactory->getExternalLB( $cluster );
164 $lb = $this->getLoadBalancer( $cluster );
166 return $lb->getConnectionRef(
169 $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
170 $lb::CONN_TRX_AUTOCOMMIT
182 $lb = $this->getLoadBalancer( $cluster );
184 return $lb->getMaintenanceConnectionRef(
187 $this->getDomainId( $lb->getServerInfo( $lb->getWriterIndex() ) ),
188 $lb::CONN_TRX_AUTOCOMMIT
206 private function getDomainId( array $server ) {
207 if ( $this->isDbDomainExplicit ) {
211 if ( isset( $server[
'dbname'] ) ) {
219 $server[
'schema'] ??
null,
220 $server[
'tablePrefix'] ??
''
223 return $domain->getId();
237 if ( $cluster !==
null ) {
238 $lb = $this->getLoadBalancer( $cluster );
239 $info = $lb->getServerInfo( $lb->getWriterIndex() );
240 if ( isset( $info[
'blobs table'] ) ) {
241 return $info[
'blobs table'];
245 return $db->getLBInfo(
'blobs table' ) ??
'blobs';
258 static $supportedTypes = [
'mysql',
'sqlite' ];
261 if ( !in_array( $dbw->getType(), $supportedTypes,
true ) ) {
262 throw new DBUnexpectedError( $dbw,
"RDBMS type '{$dbw->getType()}' not supported." );
265 $sqlFilePath =
"$IP/maintenance/storage/blobs.sql";
266 $sql = file_get_contents( $sqlFilePath );
267 if ( $sql ===
false ) {
268 throw new RuntimeException(
"Failed to read '$sqlFilePath'." );
271 $rawTable = $this->
getTable( $dbw, $cluster );
272 $encTable = $dbw->tableName( $rawTable );
275 [
'/*$wgDBprefix*/blobs',
'/*_*/blobs' ],
276 [ $encTable, $encTable ],
280 $dbw::QUERY_IGNORE_DBO_TRX
293 private function fetchBlob( $cluster, $id, $itemID ) {
300 static $externalBlobCache = [];
302 $cacheID = ( $itemID === false ) ?
"$cluster/$id" :
"$cluster/$id/";
303 $cacheID =
"$cacheID@{$this->dbDomain}";
305 if ( isset( $externalBlobCache[$cacheID] ) ) {
306 $this->logger->debug( __METHOD__ .
": cache hit on $cacheID" );
308 return $externalBlobCache[$cacheID];
311 $this->logger->debug( __METHOD__ .
": cache miss on $cacheID" );
314 $ret =
$dbr->selectField(
317 [
'blob_id' => $id ],
320 if ( $ret ===
false ) {
322 $this->logger->warning( __METHOD__ .
": primary DB fallback on $cacheID" );
323 $scope = $this->lbFactory->getTransactionProfiler()->silenceForScope();
325 $ret = $dbw->selectField(
328 [
'blob_id' => $id ],
331 ScopedCallback::consume( $scope );
332 if ( $ret ===
false ) {
333 $this->logger->warning( __METHOD__ .
": primary DB failed to find $cacheID" );
336 if ( $itemID !==
false && $ret !==
false ) {
341 $externalBlobCache = [ $cacheID => $ret ];
354 private function batchFetchBlobs( $cluster, array $ids ) {
356 $res =
$dbr->newSelectQueryBuilder()
357 ->select( [
'blob_id',
'blob_text' ] )
359 ->where( [
'blob_id' => array_keys( $ids ) ] )
360 ->caller( __METHOD__ )
364 if (
$res !==
false ) {
365 $this->mergeBatchResult( $ret, $ids,
$res );
370 __METHOD__ .
": primary fallback on '$cluster' for: " .
371 implode(
',', array_keys( $ids ) )
373 $scope = $this->lbFactory->getTransactionProfiler()->silenceForScope();
375 $res = $dbw->newSelectQueryBuilder()
376 ->select( [
'blob_id',
'blob_text' ] )
378 ->where( [
'blob_id' => array_keys( $ids ) ] )
379 ->caller( __METHOD__ )
381 ScopedCallback::consume( $scope );
382 if (
$res ===
false ) {
383 $this->logger->error( __METHOD__ .
": primary failed on '$cluster'" );
385 $this->mergeBatchResult( $ret, $ids,
$res );
389 $this->logger->error(
390 __METHOD__ .
": primary on '$cluster' failed locating items: " .
391 implode(
',', array_keys( $ids ) )
404 private function mergeBatchResult( array &$ret, array &$ids,
$res ) {
405 foreach (
$res as $row ) {
407 $itemIDs = $ids[$id];
409 if ( count( $itemIDs ) === 1 && reset( $itemIDs ) ===
false ) {
411 $ret[$id] = $row->blob_text;
424 $path = explode(
'/', $url );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Base class for general text storage via the "object" flag in old_flags, or two-part external storage ...