35use InvalidArgumentException;
39use Wikimedia\Assert\Assert;
40use Wikimedia\AtEase\AtEase;
61 private $dbLoadBalancer;
66 private $extStoreAccess;
81 private $cacheExpiry = 604800;
86 private $compressBlobs =
false;
91 private $legacyEncoding =
false;
96 private $useExternalStore =
false;
115 $this->dbLoadBalancer = $dbLoadBalancer;
116 $this->extStoreAccess = $extStoreAccess;
118 $this->dbDomain = $dbDomain;
125 return $this->cacheExpiry;
132 $this->cacheExpiry = $cacheExpiry;
139 return $this->compressBlobs;
146 $this->compressBlobs = $compressBlobs;
154 return $this->legacyEncoding;
166 $this->legacyEncoding = $legacyEncoding;
173 return $this->useExternalStore;
180 $this->useExternalStore = $useExternalStore;
186 private function getDBLoadBalancer() {
187 return $this->dbLoadBalancer;
195 private function getDBConnection( $index ) {
196 $lb = $this->getDBLoadBalancer();
197 return $lb->getConnectionRef( $index, [], $this->dbDomain );
214 # Write to external storage if required
215 if ( $this->useExternalStore ) {
217 $data = $this->extStoreAccess->insert( $data, [
'domain' => $this->dbDomain ] );
224 $flags .=
'external';
235 [
'old_text' => $data,
'old_flags' => $flags ],
239 $textId = $dbw->insertId();
259 public function getBlob( $blobAddress, $queryFlags = 0 ) {
260 Assert::parameterType(
'string', $blobAddress,
'$blobAddress' );
263 $blob = $this->cache->getWithSetCallback(
264 $this->getCacheKey( $blobAddress ),
265 $this->getCacheTTL(),
266 function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
268 list( $result, $errors ) = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
270 $error = $errors[$blobAddress] ??
null;
271 return $result[$blobAddress];
280 Assert::postcondition( is_string(
$blob ),
'Blob must not be null' );
300 list( $blobsByAddress, $errors ) = $this->fetchBlobs( $blobAddresses, $queryFlags );
302 $blobsByAddress = array_map(
static function (
$blob ) {
304 }, $blobsByAddress );
306 $result = StatusValue::newGood( $blobsByAddress );
308 foreach ( $errors as $error ) {
309 $result->warning(
'internalerror', $error );
325 private function fetchBlobs( $blobAddresses, $queryFlags ) {
326 $textIdToBlobAddress = [];
329 foreach ( $blobAddresses as $blobAddress ) {
332 }
catch ( InvalidArgumentException $ex ) {
333 throw new BlobAccessException(
334 $ex->getMessage() .
'. Use findBadBlobs.php to remedy.',
341 if ( $schema ===
'bad' ) {
345 .
": loading known-bad content ($blobAddress), returning empty string"
347 $result[$blobAddress] =
'';
349 } elseif ( $schema ===
'tt' ) {
350 $textId = intval( $id );
352 if ( $textId < 1 || $id !== (
string)$textId ) {
353 $errors[$blobAddress] =
"Bad blob address: $blobAddress."
354 .
' Use findBadBlobs.php to remedy.';
355 $result[$blobAddress] =
false;
358 $textIdToBlobAddress[$textId] = $blobAddress;
360 $errors[$blobAddress] =
"Unknown blob address schema: $schema."
361 .
' Use findBadBlobs.php to remedy.';
362 $result[$blobAddress] =
false;
366 $textIds = array_keys( $textIdToBlobAddress );
368 return [ $result, $errors ];
373 ? self::READ_LATEST_IMMUTABLE
375 list( $index, $options, $fallbackIndex, $fallbackOptions ) =
378 $dbConnection = $this->getDBConnection( $index );
379 $rows = $dbConnection->select(
381 [
'old_id',
'old_text',
'old_flags' ],
382 [
'old_id' => $textIds ],
387 if ( $rows instanceof IResultWrapper ) {
388 $numRows = $rows->numRows();
393 if ( $numRows !== count( $textIds ) && $fallbackIndex !==
null ) {
394 $fetchedTextIds = [];
395 foreach ( $rows as $row ) {
396 $fetchedTextIds[] = $row->old_id;
398 $missingTextIds = array_diff( $textIds, $fetchedTextIds );
399 $dbConnection = $this->getDBConnection( $fallbackIndex );
400 $rowsFromFallback = $dbConnection->select(
402 [
'old_id',
'old_text',
'old_flags' ],
403 [
'old_id' => $missingTextIds ],
407 $appendIterator =
new AppendIterator();
408 $appendIterator->append( $rows );
409 $appendIterator->append( $rowsFromFallback );
410 $rows = $appendIterator;
413 foreach ( $rows as $row ) {
414 $blobAddress = $textIdToBlobAddress[$row->old_id];
416 if ( $row->old_text !==
null ) {
417 $blob = $this->
expandBlob( $row->old_text, $row->old_flags, $blobAddress );
419 if (
$blob ===
false ) {
420 $errors[$blobAddress] =
"Bad data in text row {$row->old_id}."
421 .
' Use findBadBlobs.php to remedy.';
423 $result[$blobAddress] =
$blob;
427 if ( count( $result ) !== count( $blobAddresses ) ) {
428 foreach ( $blobAddresses as $blobAddress ) {
429 if ( !isset( $result[$blobAddress ] ) ) {
430 $errors[$blobAddress] =
"Unable to fetch blob at $blobAddress."
431 .
' Use findBadBlobs.php to remedy.';
432 $result[$blobAddress] =
false;
436 return [ $result, $errors ];
449 private function getCacheKey( $blobAddress ) {
450 return $this->cache->makeGlobalKey(
452 $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
476 public function expandBlob( $raw, $flags, $cacheKey =
null ) {
477 if ( is_string( $flags ) ) {
478 $flags = explode(
',', $flags );
482 if ( in_array(
'external', $flags ) ) {
484 $parts = explode(
'://', $url, 2 );
485 if ( count( $parts ) == 1 || $parts[1] ==
'' ) {
491 return $this->cache->getWithSetCallback(
492 $this->getCacheKey( $cacheKey ),
493 $this->getCacheTTL(),
494 function () use ( $url, $flags ) {
496 $blob = $this->extStoreAccess
497 ->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
504 $blob = $this->extStoreAccess->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
534 $blobFlags[] =
'utf-8';
536 if ( $this->compressBlobs ) {
537 if ( function_exists(
'gzdeflate' ) ) {
538 $deflated = gzdeflate(
$blob );
540 if ( $deflated ===
false ) {
544 $blobFlags[] =
'gzip';
547 wfDebug( __METHOD__ .
" -- no zlib support, not compressing" );
550 return implode(
',', $blobFlags );
569 if ( in_array(
'error', $blobFlags ) ) {
574 if ( in_array(
'gzip', $blobFlags ) ) {
575 # Deal with optional compression of archived pages.
576 # This can be done periodically via maintenance/compressOld.php, and
577 # as pages are saved if $wgCompressRevisions is set.
580 if (
$blob ===
false ) {
581 wfWarn( __METHOD__ .
': gzinflate() failed' );
586 if ( in_array(
'object', $blobFlags ) ) {
587 # Generic compressed storage
589 if ( !is_object( $obj ) ) {
593 $blob = $obj->getText();
597 if (
$blob !==
false && $this->legacyEncoding
598 && !in_array(
'utf-8', $blobFlags ) && !in_array(
'utf8', $blobFlags )
600 # Old revisions kept around in a legacy encoding?
601 # Upconvert on demand.
602 # ("utf8" checked for compatibility with some broken
603 # conversion scripts 2008-12-30)
605 # *input* string. We just ignore those too.
608 AtEase::suppressWarnings();
609 $blob = iconv( $this->legacyEncoding,
'UTF-8//IGNORE',
$blob );
610 AtEase::restoreWarnings();
623 private function getCacheTTL() {
626 if (
$cache->getQoS( $cache::ATTR_DURABILITY ) >= $cache::QOS_DURABILITY_RDBMS ) {
628 $ttl = $cache::TTL_UNCACHEABLE;
630 $ttl = $this->cacheExpiry ?: $cache::TTL_UNCACHEABLE;
659 if ( $schema !==
'tt' ) {
663 $textId = intval( $id );
665 if ( !$textId || $id !== (
string)$textId ) {
666 throw new InvalidArgumentException(
"Malformed text_id: $id" );
700 if ( !preg_match(
'/^([-+.\w]+):([^\s?]+)(\?([^\s]*))?$/', $address, $m ) ) {
701 throw new InvalidArgumentException(
"Bad blob address: $address" );
704 $schema = strtolower( $m[1] );
708 return [ $schema, $id, $parameters ];
712 if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
716 return ( $this->getDBLoadBalancer()->getReadOnlyReason() !==
false );
unserialize( $serialized)
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
This is the main interface for fetching or inserting objects with ExternalStore.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Multi-datacenter aware caching interface.
Interface for database access objects.
Generic interface providing TTL constants for lightweight expiring object stores.