47 private $dbLoadBalancer;
52 private $extStoreAccess;
72 private $compressBlobs =
false;
77 private $legacyEncoding =
false;
82 private $useExternalStore =
false;
101 $this->dbLoadBalancer = $dbLoadBalancer;
102 $this->extStoreAccess = $extStoreAccess;
103 $this->cache = $cache;
104 $this->dbDomain = $dbDomain;
111 return $this->cacheExpiry;
118 $this->cacheExpiry = $cacheExpiry;
125 return $this->compressBlobs;
132 $this->compressBlobs = $compressBlobs;
140 return $this->legacyEncoding;
152 $this->legacyEncoding = $legacyEncoding;
159 return $this->useExternalStore;
166 $this->useExternalStore = $useExternalStore;
172 private function getDBLoadBalancer() {
173 return $this->dbLoadBalancer;
181 private function getDBConnection( $index ) {
182 $lb = $this->getDBLoadBalancer();
183 return $lb->getConnection( $index, [], $this->dbDomain );
199 # Write to external storage if required
200 if ( $this->useExternalStore ) {
203 $data = $this->extStoreAccess->insert( $data, [
'domain' => $this->dbDomain ] );
211 return 'es:' . $data .
'?flags=' . $flags;
213 return 'es:' . $data;
218 $dbw->newInsertQueryBuilder()
219 ->insertInto(
'text' )
220 ->row( [
'old_text' => $data,
'old_flags' => $flags ] )
221 ->caller( __METHOD__ )->execute();
223 $textId = $dbw->insertId();
241 public function getBlob( $blobAddress, $queryFlags = 0 ) {
242 Assert::parameterType(
'string', $blobAddress,
'$blobAddress' );
245 $blob = $this->cache->getWithSetCallback(
246 $this->getCacheKey( $blobAddress ),
247 $this->getCacheTTL(),
248 function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
250 [ $result, $errors ] = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
252 $error = $errors[$blobAddress] ??
null;
254 $ttl = WANObjectCache::TTL_UNCACHEABLE;
256 return $result[$blobAddress];
258 $this->getCacheOptions()
262 if ( $error[0] ===
'badrevision' ) {
269 Assert::postcondition( is_string( $blob ),
'Blob must not be null' );
289 [ $blobsByAddress, $errors ] = $this->fetchBlobs( $blobAddresses, $queryFlags );
291 $blobsByAddress = array_map(
static function ( $blob ) {
292 return $blob ===
false ? null : $blob;
293 }, $blobsByAddress );
295 $result = StatusValue::newGood( $blobsByAddress );
296 foreach ( $errors as $error ) {
298 $result->warning( ...$error );
317 private function fetchBlobs( $blobAddresses, $queryFlags ) {
318 $textIdToBlobAddress = [];
321 foreach ( $blobAddresses as $blobAddress ) {
324 }
catch ( InvalidArgumentException $ex ) {
325 throw new BlobAccessException(
326 $ex->getMessage() .
'. Use findBadBlobs.php to remedy.',
332 if ( $schema ===
'es' ) {
333 if ( $params && isset( $params[
'flags'] ) ) {
334 $blob = $this->
expandBlob( $id, $params[
'flags'] .
',external', $blobAddress );
336 $blob = $this->
expandBlob( $id,
'external', $blobAddress );
339 if ( $blob ===
false ) {
340 $errors[$blobAddress] = [
342 "Bad data in external store address $id. Use findBadBlobs.php to remedy."
345 $result[$blobAddress] = $blob;
346 } elseif ( $schema ===
'bad' ) {
350 .
": loading known-bad content ($blobAddress), returning empty string"
352 $result[$blobAddress] =
'';
353 $errors[$blobAddress] = [
355 'The content of this revision is missing or corrupted (bad schema)'
357 } elseif ( $schema ===
'tt' ) {
358 $textId = intval( $id );
360 if ( $textId < 1 || $id !== (
string)$textId ) {
361 $errors[$blobAddress] = [
363 "Bad blob address: $blobAddress. Use findBadBlobs.php to remedy."
365 $result[$blobAddress] =
false;
368 $textIdToBlobAddress[$textId] = $blobAddress;
370 $errors[$blobAddress] = [
372 "Unknown blob address schema: $schema. Use findBadBlobs.php to remedy."
374 $result[$blobAddress] =
false;
378 $textIds = array_keys( $textIdToBlobAddress );
380 return [ $result, $errors ];
384 $queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST )
385 ? IDBAccessObject::READ_LATEST_IMMUTABLE
387 [ $index, $options, $fallbackIndex, $fallbackOptions ] =
388 self::getDBOptions( $queryFlags );
390 $dbConnection = $this->getDBConnection( $index );
391 $rows = $dbConnection->newSelectQueryBuilder()
392 ->select( [
'old_id',
'old_text',
'old_flags' ] )
394 ->where( [
'old_id' => $textIds ] )
395 ->options( $options )
396 ->caller( __METHOD__ )->fetchResultSet();
397 $numRows = $rows->numRows();
401 if ( $numRows !== count( $textIds ) && $fallbackIndex !==
null ) {
402 $fetchedTextIds = [];
403 foreach ( $rows as $row ) {
404 $fetchedTextIds[] = $row->old_id;
406 $missingTextIds = array_diff( $textIds, $fetchedTextIds );
407 $dbConnection = $this->getDBConnection( $fallbackIndex );
408 $rowsFromFallback = $dbConnection->newSelectQueryBuilder()
409 ->select( [
'old_id',
'old_text',
'old_flags' ] )
411 ->where( [
'old_id' => $missingTextIds ] )
412 ->options( $fallbackOptions )
413 ->caller( __METHOD__ )->fetchResultSet();
414 $appendIterator =
new AppendIterator();
415 $appendIterator->append( $rows );
416 $appendIterator->append( $rowsFromFallback );
417 $rows = $appendIterator;
420 foreach ( $rows as $row ) {
421 $blobAddress = $textIdToBlobAddress[$row->old_id];
423 if ( $row->old_text !==
null ) {
424 $blob = $this->
expandBlob( $row->old_text, $row->old_flags, $blobAddress );
426 if ( $blob ===
false ) {
427 $errors[$blobAddress] = [
429 "Bad data in text row {$row->old_id}. Use findBadBlobs.php to remedy."
432 $result[$blobAddress] = $blob;
436 if ( count( $result ) !== count( $blobAddresses ) ) {
437 foreach ( $blobAddresses as $blobAddress ) {
438 if ( !isset( $result[$blobAddress ] ) ) {
439 $errors[$blobAddress] = [
441 "Unable to fetch blob at $blobAddress. Use findBadBlobs.php to remedy."
443 $result[$blobAddress] =
false;
447 return [ $result, $errors ];
450 private static function getDBOptions(
int $bitfield ): array {
451 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
454 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST ) ) {
456 $fallbackIndex =
null;
459 $fallbackIndex =
null;
462 $lockingOptions = [];
463 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_EXCLUSIVE ) ) {
464 $lockingOptions[] =
'FOR UPDATE';
465 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LOCKING ) ) {
466 $lockingOptions[] =
'LOCK IN SHARE MODE';
469 if ( $fallbackIndex !==
null ) {
471 $fallbackOptions = $lockingOptions;
473 $options = $lockingOptions;
474 $fallbackOptions = [];
477 return [ $index, $options, $fallbackIndex, $fallbackOptions ];
491 return $this->cache->makeGlobalKey(
493 $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
503 private function getCacheOptions() {
505 'pcGroup' => self::TEXT_CACHE_GROUP,
506 'pcTTL' => WANObjectCache::TTL_PROC_LONG,
507 'segmentable' => true
531 public function expandBlob( $raw, $flags, $blobAddress =
null ) {
532 if ( is_string( $flags ) ) {
533 $flags = self::explodeFlags( $flags );
535 if ( in_array(
'error', $flags ) ) {
537 "The content of this revision is missing or corrupted (error flag)"
542 if ( in_array(
'external', $flags ) ) {
544 $parts = explode(
'://',
$url, 2 );
545 if ( count( $parts ) == 1 || $parts[1] ==
'' ) {
549 if ( $blobAddress ) {
551 return $this->cache->getWithSetCallback(
552 $this->getCacheKey( $blobAddress ),
553 $this->getCacheTTL(),
554 function () use (
$url, $flags, $blobAddress ) {
556 $blob = $this->extStoreAccess
557 ->fetchFromURL(
$url, [
'domain' => $this->dbDomain ] );
559 return $blob ===
false ? false : $this->decompressData( $blob, $flags, $blobAddress );
561 $this->getCacheOptions()
564 $blob = $this->extStoreAccess->fetchFromURL(
$url, [
'domain' => $this->dbDomain ] );
565 return $blob ===
false ? false : $this->decompressData( $blob, $flags, $blobAddress );
568 return $this->decompressData( $raw, $flags, $blobAddress );
594 $blobFlags[] =
'utf-8';
596 if ( $this->compressBlobs ) {
597 if ( function_exists(
'gzdeflate' ) ) {
598 $deflated = gzdeflate( $blob );
600 if ( $deflated ===
false ) {
604 $blobFlags[] =
'gzip';
607 wfDebug( __METHOD__ .
" -- no zlib support, not compressing" );
610 return implode(
',', $blobFlags );
629 public function decompressData(
string $blob, array $blobFlags, ?
string $blobAddress =
null ) {
630 if ( in_array(
'error', $blobFlags ) ) {
638 if ( in_array(
'gzip', $blobFlags ) ) {
641 $blob = @gzinflate( $blob );
642 if ( $blob ===
false ) {
643 wfWarn( __METHOD__ .
': gzinflate() failed' .
644 ( $blobAddress ?
' (at blob address ' . $blobAddress .
')' :
'' ) );
649 if ( in_array(
'object', $blobFlags ) ) {
650 # Generic compressed storage
651 $obj = HistoryBlobUtils::unserialize( $blob );
656 $blob = $obj->getText();
660 if ( $blob !==
false && $this->legacyEncoding
661 && !in_array(
'utf-8', $blobFlags ) && !in_array(
'utf8', $blobFlags )
673 $blob = @iconv( $this->legacyEncoding,
'UTF-8//IGNORE', $blob );
686 private function getCacheTTL() {
687 $cache = $this->cache;
689 if ( $cache->
getQoS( BagOStuff::ATTR_DURABILITY ) >= BagOStuff::QOS_DURABILITY_RDBMS ) {
691 $ttl = $cache::TTL_UNCACHEABLE;
693 $ttl = $this->cacheExpiry ?: $cache::TTL_UNCACHEABLE;
720 [ $schema, $id, ] = self::splitBlobAddress( $address );
722 if ( $schema !==
'tt' ) {
726 $textId = intval( $id );
728 if ( !$textId || $id !== (
string)$textId ) {
729 throw new InvalidArgumentException(
"Malformed text_id: $id" );
759 return $flagsString ===
'' ? [] : explode(
',', $flagsString );
772 if ( !preg_match(
'/^([-+.\w]+):([^\s?]+)(\?([^\s]*))?$/', $address, $m ) ) {
773 throw new InvalidArgumentException(
"Bad blob address: $address" );
776 $schema = strtolower( $m[1] );
780 return [ $schema, $id, $parameters ];
785 if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
789 return ( $this->getDBLoadBalancer()->getReadOnlyReason() !==
false );