X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=blobdiff_plain;f=includes%2Flibs%2Ffilebackend%2FFileBackendMultiWrite.php;h=cbfd76e407196abc5d5b4b23364308b309405ec7;hp=27ad870ae59927b313af515eab7d642a757e390e;hb=e390198c4e4be7632b01173e42050061f1cc346a;hpb=5264862bc1224fbb2eec487155aa0253af1fa777 diff --git a/includes/libs/filebackend/FileBackendMultiWrite.php b/includes/libs/filebackend/FileBackendMultiWrite.php index 27ad870ae5..cbfd76e407 100644 --- a/includes/libs/filebackend/FileBackendMultiWrite.php +++ b/includes/libs/filebackend/FileBackendMultiWrite.php @@ -21,6 +21,8 @@ * @ingroup FileBackend */ +use Wikimedia\Timestamp\ConvertibleTimestamp; + /** * @brief Proxy backend that mirrors writes to several internal backends. * @@ -57,9 +59,11 @@ class FileBackendMultiWrite extends FileBackend { /** @var bool */ protected $asyncWrites = false; - /* Possible internal backend consistency checks */ + /** @var int Compare file sizes among backends */ const CHECK_SIZE = 1; + /** @var int Compare file mtimes among backends */ const CHECK_TIME = 2; + /** @var int Compare file hashes among backends */ const CHECK_SHA1 = 4; /** @@ -139,10 +143,8 @@ class FileBackendMultiWrite extends FileBackend { $mbe = $this->backends[$this->masterIndex]; // convenience - // Try to lock those files for the scope of this function... - $scopeLock = null; + // Acquire any locks as needed if ( empty( $opts['nonLocking'] ) ) { - // Try to lock those files for the scope of this function... /** @noinspection PhpUnusedLocalVariableInspection */ $scopeLock = $this->getScopedLocksForOps( $ops, $status ); if ( !$status->isOK() ) { @@ -152,28 +154,30 @@ class FileBackendMultiWrite extends FileBackend { // Clear any cache entries (after locks acquired) $this->clearCache(); $opts['preserveCache'] = true; // only locked files are cached - // Get the list of paths to read/write... + // Get the list of paths to read/write $relevantPaths = $this->fileStoragePathsForOps( $ops ); - // Check if the paths are valid and accessible on all backends... + // Check if the paths are valid and accessible on all backends $status->merge( $this->accessibilityCheck( $relevantPaths ) ); if ( !$status->isOK() ) { return $status; // abort } - // Do a consistency check to see if the backends are consistent... + // Do a consistency check to see if the backends are consistent $syncStatus = $this->consistencyCheck( $relevantPaths ); if ( !$syncStatus->isOK() ) { - wfDebugLog( 'FileOperation', static::class . - " failed sync check: " . FormatJson::encode( $relevantPaths ) ); - // Try to resync the clone backends to the master on the spot... - if ( $this->autoResync === false - || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() + $this->logger->error( + __METHOD__ . ": failed sync check: " . FormatJson::encode( $relevantPaths ) + ); + // Try to resync the clone backends to the master on the spot + if ( + $this->autoResync === false || + !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() ) { $status->merge( $syncStatus ); return $status; // abort } } - // Actually attempt the operation batch on the master backend... + // Actually attempt the operation batch on the master backend $realOps = $this->substOpBatchPaths( $ops, $mbe ); $masterStatus = $mbe->doOperations( $realOps, $opts ); $status->merge( $masterStatus ); @@ -191,16 +195,18 @@ class FileBackendMultiWrite extends FileBackend { // Bind $scopeLock to the callback to preserve locks DeferredUpdates::addCallableUpdate( function () use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) { - wfDebugLog( 'FileOperationReplication', + $this->logger->error( "'{$backend->getName()}' async replication; paths: " . - FormatJson::encode( $relevantPaths ) ); + FormatJson::encode( $relevantPaths ) + ); $backend->doOperations( $realOps, $opts ); } ); } else { - wfDebugLog( 'FileOperationReplication', + $this->logger->error( "'{$backend->getName()}' sync replication; paths: " . - FormatJson::encode( $relevantPaths ) ); + FormatJson::encode( $relevantPaths ) + ); $status->merge( $backend->doOperations( $realOps, $opts ) ); } } @@ -218,6 +224,9 @@ class FileBackendMultiWrite extends FileBackend { /** * Check that a set of files are consistent across all internal backends * + * This method should only be called if the files are locked or the backend + * is in read-only mode + * * @param array $paths List of storage paths * @return StatusValue */ @@ -227,58 +236,75 @@ class FileBackendMultiWrite extends FileBackend { return $status; // skip checks } - // Preload all of the stat info in as few round trips as possible... + // Preload all of the stat info in as few round trips as possible foreach ( $this->backends as $backend ) { $realPaths = $this->substPaths( $paths, $backend ); $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] ); } - $mBackend = $this->backends[$this->masterIndex]; foreach ( $paths as $path ) { $params = [ 'src' => $path, 'latest' => true ]; - $mParams = $this->substOpPaths( $params, $mBackend ); - // Stat the file on the 'master' backend - $mStat = $mBackend->getFileStat( $mParams ); + // Get the state of the file on the master backend + $masterBackend = $this->backends[$this->masterIndex]; + $masterParams = $this->substOpPaths( $params, $masterBackend ); + $masterStat = $masterBackend->getFileStat( $masterParams ); + if ( $masterStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + continue; + } if ( $this->syncChecks & self::CHECK_SHA1 ) { - $mSha1 = $mBackend->getFileSha1Base36( $mParams ); + $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); + if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { + $status->fatal( 'backend-fail-hash', $path ); + continue; + } } else { - $mSha1 = false; + $masterSha1 = null; // unused } + // Check if all clone backends agree with the master... - foreach ( $this->backends as $index => $cBackend ) { + foreach ( $this->backends as $index => $cloneBackend ) { if ( $index === $this->masterIndex ) { continue; // master } - $cParams = $this->substOpPaths( $params, $cBackend ); - $cStat = $cBackend->getFileStat( $cParams ); - if ( $mStat ) { // file is in master - if ( !$cStat ) { // file should exist + + // Get the state of the file on the clone backend + $cloneParams = $this->substOpPaths( $params, $cloneBackend ); + $cloneStat = $cloneBackend->getFileStat( $cloneParams ); + + if ( $masterStat ) { + // File exists in the master backend + if ( !$cloneStat ) { + // File is missing from the clone backend $status->fatal( 'backend-fail-synced', $path ); - continue; - } - if ( ( $this->syncChecks & self::CHECK_SIZE ) - && $cStat['size'] != $mStat['size'] - ) { // wrong size + } elseif ( + ( $this->syncChecks & self::CHECK_SIZE ) && + $cloneStat['size'] !== $masterStat['size'] + ) { + // File in the clone backend is different $status->fatal( 'backend-fail-synced', $path ); - continue; - } - if ( $this->syncChecks & self::CHECK_TIME ) { - $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] ); - $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] ); - if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere - $status->fatal( 'backend-fail-synced', $path ); - continue; - } - } - if ( + } elseif ( + ( $this->syncChecks & self::CHECK_TIME ) && + abs( + ConvertibleTimestamp::convert( TS_UNIX, $masterStat['mtime'] ) - + ConvertibleTimestamp::convert( TS_UNIX, $cloneStat['mtime'] ) + ) > 30 + ) { + // File in the clone backend is significantly newer or older + $status->fatal( 'backend-fail-synced', $path ); + } elseif ( ( $this->syncChecks & self::CHECK_SHA1 ) && - $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 - ) { // wrong SHA1 + $cloneBackend->getFileSha1Base36( $cloneParams ) !== $masterSha1 + ) { + // File in the clone backend is different + $status->fatal( 'backend-fail-synced', $path ); + } + } else { + // File does not exist in the master backend + if ( $cloneStat ) { + // Stray file exists in the clone backend $status->fatal( 'backend-fail-synced', $path ); - continue; } - } elseif ( $cStat ) { // file is not in master; file should not exist - $status->fatal( 'backend-fail-synced', $path ); } } } @@ -314,6 +340,8 @@ class FileBackendMultiWrite extends FileBackend { * Check that a set of files are consistent across all internal backends * and re-synchronize those files against the "multi master" if needed. * + * This method should only be called if the files are locked + * * @param array $paths List of storage paths * @param string|bool $resyncMode False, True, or "conservative"; see __construct() * @return StatusValue @@ -321,58 +349,83 @@ class FileBackendMultiWrite extends FileBackend { public function resyncFiles( array $paths, $resyncMode = true ) { $status = $this->newStatus(); - $mBackend = $this->backends[$this->masterIndex]; + $fname = __METHOD__; foreach ( $paths as $path ) { - $mPath = $this->substPaths( $path, $mBackend ); - $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] ); - $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] ); - if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity - $status->fatal( 'backend-fail-internal', $this->name ); - wfDebugLog( 'FileOperation', __METHOD__ - . ': File is not available on the master backend' ); - continue; // file is not available on the master backend... + $params = [ 'src' => $path, 'latest' => true ]; + // Get the state of the file on the master backend + $masterBackend = $this->backends[$this->masterIndex]; + $masterParams = $this->substOpPaths( $params, $masterBackend ); + $masterPath = $masterParams['src']; + $masterStat = $masterBackend->getFileStat( $masterParams ); + if ( $masterStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + $this->logger->error( "$fname: file '$masterPath' is not available" ); + continue; + } + $masterSha1 = $masterBackend->getFileSha1Base36( $masterParams ); + if ( ( $masterSha1 !== false ) !== (bool)$masterStat ) { + $status->fatal( 'backend-fail-hash', $path ); + $this->logger->error( "$fname: file '$masterPath' hash does not match stat" ); + continue; } + // Check of all clone backends agree with the master... - foreach ( $this->backends as $index => $cBackend ) { + foreach ( $this->backends as $index => $cloneBackend ) { if ( $index === $this->masterIndex ) { continue; // master } - $cPath = $this->substPaths( $path, $cBackend ); - $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] ); - $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] ); - if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity - $status->fatal( 'backend-fail-internal', $cBackend->getName() ); - wfDebugLog( 'FileOperation', __METHOD__ . - ': File is not available on the clone backend' ); - continue; // file is not available on the clone backend... + + // Get the state of the file on the clone backend + $cloneParams = $this->substOpPaths( $params, $cloneBackend ); + $clonePath = $cloneParams['src']; + $cloneStat = $cloneBackend->getFileStat( $cloneParams ); + if ( $cloneStat === self::STAT_ERROR ) { + $status->fatal( 'backend-fail-stat', $path ); + $this->logger->error( "$fname: file '$clonePath' is not available" ); + continue; } - if ( $mSha1 === $cSha1 ) { - // already synced; nothing to do - } elseif ( $mSha1 !== false ) { // file is in master - if ( $resyncMode === 'conservative' - && $cStat && $cStat['mtime'] > $mStat['mtime'] + $cloneSha1 = $cloneBackend->getFileSha1Base36( $cloneParams ); + if ( ( $cloneSha1 !== false ) !== (bool)$cloneStat ) { + $status->fatal( 'backend-fail-hash', $path ); + $this->logger->error( "$fname: file '$clonePath' hash does not match stat" ); + continue; + } + + if ( $masterSha1 === $cloneSha1 ) { + // File is either the same in both backends or absent from both backends + $this->logger->debug( "$fname: file '$clonePath' matches '$masterPath'" ); + } elseif ( $masterSha1 !== false ) { + // File is either missing from or different in the clone backend + if ( + $resyncMode === 'conservative' && + $cloneStat && + $cloneStat['mtime'] > $masterStat['mtime'] ) { + // Do not replace files with older ones; reduces the risk of data loss $status->fatal( 'backend-fail-synced', $path ); - continue; // don't rollback data + } else { + // Copy the master backend file to the clone backend in overwrite mode + $fsFile = $masterBackend->getLocalReference( $masterParams ); + $status->merge( $cloneBackend->quickStore( [ + 'src' => $fsFile, + 'dst' => $clonePath + ] ) ); } - $fsFile = $mBackend->getLocalReference( - [ 'src' => $mPath, 'latest' => true ] ); - $status->merge( $cBackend->quickStore( - [ 'src' => $fsFile->getPath(), 'dst' => $cPath ] - ) ); - } elseif ( $mStat === false ) { // file is not in master + } elseif ( $masterStat === false ) { + // Stray file exists in the clone backend if ( $resyncMode === 'conservative' ) { + // Do not delete stray files; reduces the risk of data loss $status->fatal( 'backend-fail-synced', $path ); - continue; // don't delete data + } else { + // Delete the stay file from the clone backend + $status->merge( $cloneBackend->quickDelete( [ 'src' => $clonePath ] ) ); } - $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) ); } } } if ( !$status->isOK() ) { - wfDebugLog( 'FileOperation', static::class . - " failed to resync: " . FormatJson::encode( $paths ) ); + $this->logger->error( "$fname: failed to resync: " . FormatJson::encode( $paths ) ); } return $status; @@ -448,7 +501,7 @@ class FileBackendMultiWrite extends FileBackend { * * @param array|string $paths List of paths or single string path * @param FileBackendStore $backend - * @return array|string + * @return string[]|string */ protected function substPaths( $paths, FileBackendStore $backend ) { return preg_replace( @@ -462,12 +515,13 @@ class FileBackendMultiWrite extends FileBackend { * Substitute the backend of internal storage paths with the proxy backend's name * * @param array|string $paths List of paths or single string path - * @return array|string + * @param FileBackendStore $backend internal storage backend + * @return string[]|string */ - protected function unsubstPaths( $paths ) { + protected function unsubstPaths( $paths, FileBackendStore $backend ) { return preg_replace( - '!^mwstore://([^/]+)!', - StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ), + '!^mwstore://' . preg_quote( $backend->getName(), '!' ) . '/!', + StringUtils::escapeRegexReplacement( "mwstore://{$this->name}/" ), $paths // string or array ); } @@ -488,7 +542,7 @@ class FileBackendMultiWrite extends FileBackend { protected function doQuickOperationsInternal( array $ops ) { $status = $this->newStatus(); - // Do the operations on the master backend; setting StatusValue fields... + // Do the operations on the master backend; setting StatusValue fields $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps ); $status->merge( $masterStatus ); @@ -621,7 +675,7 @@ class FileBackendMultiWrite extends FileBackend { $contents = []; // (path => FSFile) mapping using the proxy backend's name foreach ( $contentsM as $path => $data ) { - $contents[$this->unsubstPaths( $path )] = $data; + $contents[$this->unsubstPaths( $path, $this->backends[$index] )] = $data; } return $contents; @@ -656,7 +710,7 @@ class FileBackendMultiWrite extends FileBackend { $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name foreach ( $fsFilesM as $path => $fsFile ) { - $fsFiles[$this->unsubstPaths( $path )] = $fsFile; + $fsFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $fsFile; } return $fsFiles; @@ -670,7 +724,7 @@ class FileBackendMultiWrite extends FileBackend { $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name foreach ( $tempFilesM as $path => $tempFile ) { - $tempFiles[$this->unsubstPaths( $path )] = $tempFile; + $tempFiles[$this->unsubstPaths( $path, $this->backends[$index] )] = $tempFile; } return $tempFiles; @@ -731,8 +785,14 @@ class FileBackendMultiWrite extends FileBackend { $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps ); // Get the paths under the proxy backend's name $pbPaths = [ - LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ), - LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] ) + LockManager::LOCK_UW => $this->unsubstPaths( + $paths[LockManager::LOCK_UW], + $this->backends[$this->masterIndex] + ), + LockManager::LOCK_EX => $this->unsubstPaths( + $paths[LockManager::LOCK_EX], + $this->backends[$this->masterIndex] + ) ]; // Actually acquire the locks