MediaWiki  REL1_24
FileBackendMultiWrite.php
Go to the documentation of this file.
00001 <?php
00042 class FileBackendMultiWrite extends FileBackend {
00046     protected $backends = array();
00047 
00049     protected $masterIndex = -1;
00050 
00052     protected $syncChecks = 0;
00053 
00055     protected $autoResync = false;
00056 
00058     protected $noPushDirConts = array();
00059 
00061     protected $noPushQuickOps = false;
00062 
00063     /* Possible internal backend consistency checks */
00064     const CHECK_SIZE = 1;
00065     const CHECK_TIME = 2;
00066     const CHECK_SHA1 = 4;
00067 
00097     public function __construct( array $config ) {
00098         parent::__construct( $config );
00099         $this->syncChecks = isset( $config['syncChecks'] )
00100             ? $config['syncChecks']
00101             : self::CHECK_SIZE;
00102         $this->autoResync = isset( $config['autoResync'] )
00103             ? $config['autoResync']
00104             : false;
00105         $this->noPushQuickOps = isset( $config['noPushQuickOps'] )
00106             ? $config['noPushQuickOps']
00107             : false;
00108         $this->noPushDirConts = isset( $config['noPushDirConts'] )
00109             ? $config['noPushDirConts']
00110             : array();
00111         // Construct backends here rather than via registration
00112         // to keep these backends hidden from outside the proxy.
00113         $namesUsed = array();
00114         foreach ( $config['backends'] as $index => $config ) {
00115             if ( isset( $config['template'] ) ) {
00116                 // Config is just a modified version of a registered backend's.
00117                 // This should only be used when that config is used only by this backend.
00118                 $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
00119             }
00120             $name = $config['name'];
00121             if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
00122                 throw new FileBackendError( "Two or more backends defined with the name $name." );
00123             }
00124             $namesUsed[$name] = 1;
00125             // Alter certain sub-backend settings for sanity
00126             unset( $config['readOnly'] ); // use proxy backend setting
00127             unset( $config['fileJournal'] ); // use proxy backend journal
00128             unset( $config['lockManager'] ); // lock under proxy backend
00129             $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID
00130             if ( !empty( $config['isMultiMaster'] ) ) {
00131                 if ( $this->masterIndex >= 0 ) {
00132                     throw new FileBackendError( 'More than one master backend defined.' );
00133                 }
00134                 $this->masterIndex = $index; // this is the "master"
00135                 $config['fileJournal'] = $this->fileJournal; // log under proxy backend
00136             }
00137             // Create sub-backend object
00138             if ( !isset( $config['class'] ) ) {
00139                 throw new FileBackendError( 'No class given for a backend config.' );
00140             }
00141             $class = $config['class'];
00142             $this->backends[$index] = new $class( $config );
00143         }
00144         if ( $this->masterIndex < 0 ) { // need backends and must have a master
00145             throw new FileBackendError( 'No master backend defined.' );
00146         }
00147     }
00148 
00149     final protected function doOperationsInternal( array $ops, array $opts ) {
00150         $status = Status::newGood();
00151 
00152         $mbe = $this->backends[$this->masterIndex]; // convenience
00153 
00154         // Try to lock those files for the scope of this function...
00155         if ( empty( $opts['nonLocking'] ) ) {
00156             // Try to lock those files for the scope of this function...
00157             $scopeLock = $this->getScopedLocksForOps( $ops, $status );
00158             if ( !$status->isOK() ) {
00159                 return $status; // abort
00160             }
00161         }
00162         // Clear any cache entries (after locks acquired)
00163         $this->clearCache();
00164         $opts['preserveCache'] = true; // only locked files are cached
00165         // Get the list of paths to read/write...
00166         $relevantPaths = $this->fileStoragePathsForOps( $ops );
00167         // Check if the paths are valid and accessible on all backends...
00168         $status->merge( $this->accessibilityCheck( $relevantPaths ) );
00169         if ( !$status->isOK() ) {
00170             return $status; // abort
00171         }
00172         // Do a consistency check to see if the backends are consistent...
00173         $syncStatus = $this->consistencyCheck( $relevantPaths );
00174         if ( !$syncStatus->isOK() ) {
00175             wfDebugLog( 'FileOperation', get_class( $this ) .
00176                 " failed sync check: " . FormatJson::encode( $relevantPaths ) );
00177             // Try to resync the clone backends to the master on the spot...
00178             if ( !$this->autoResync || !$this->resyncFiles( $relevantPaths )->isOK() ) {
00179                 $status->merge( $syncStatus );
00180 
00181                 return $status; // abort
00182             }
00183         }
00184         // Actually attempt the operation batch on the master backend...
00185         $realOps = $this->substOpBatchPaths( $ops, $mbe );
00186         $masterStatus = $mbe->doOperations( $realOps, $opts );
00187         $status->merge( $masterStatus );
00188         // Propagate the operations to the clone backends if there were no unexpected errors
00189         // and if there were either no expected errors or if the 'force' option was used.
00190         // However, if nothing succeeded at all, then don't replicate any of the operations.
00191         // If $ops only had one operation, this might avoid backend sync inconsistencies.
00192         if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
00193             foreach ( $this->backends as $index => $backend ) {
00194                 if ( $index !== $this->masterIndex ) { // not done already
00195                     $realOps = $this->substOpBatchPaths( $ops, $backend );
00196                     $status->merge( $backend->doOperations( $realOps, $opts ) );
00197                 }
00198             }
00199         }
00200         // Make 'success', 'successCount', and 'failCount' fields reflect
00201         // the overall operation, rather than all the batches for each backend.
00202         // Do this by only using success values from the master backend's batch.
00203         $status->success = $masterStatus->success;
00204         $status->successCount = $masterStatus->successCount;
00205         $status->failCount = $masterStatus->failCount;
00206 
00207         return $status;
00208     }
00209 
00216     public function consistencyCheck( array $paths ) {
00217         $status = Status::newGood();
00218         if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
00219             return $status; // skip checks
00220         }
00221 
00222         $mBackend = $this->backends[$this->masterIndex];
00223         foreach ( $paths as $path ) {
00224             $params = array( 'src' => $path, 'latest' => true );
00225             $mParams = $this->substOpPaths( $params, $mBackend );
00226             // Stat the file on the 'master' backend
00227             $mStat = $mBackend->getFileStat( $mParams );
00228             if ( $this->syncChecks & self::CHECK_SHA1 ) {
00229                 $mSha1 = $mBackend->getFileSha1Base36( $mParams );
00230             } else {
00231                 $mSha1 = false;
00232             }
00233             // Check if all clone backends agree with the master...
00234             foreach ( $this->backends as $index => $cBackend ) {
00235                 if ( $index === $this->masterIndex ) {
00236                     continue; // master
00237                 }
00238                 $cParams = $this->substOpPaths( $params, $cBackend );
00239                 $cStat = $cBackend->getFileStat( $cParams );
00240                 if ( $mStat ) { // file is in master
00241                     if ( !$cStat ) { // file should exist
00242                         $status->fatal( 'backend-fail-synced', $path );
00243                         continue;
00244                     }
00245                     if ( $this->syncChecks & self::CHECK_SIZE ) {
00246                         if ( $cStat['size'] != $mStat['size'] ) { // wrong size
00247                             $status->fatal( 'backend-fail-synced', $path );
00248                             continue;
00249                         }
00250                     }
00251                     if ( $this->syncChecks & self::CHECK_TIME ) {
00252                         $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
00253                         $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
00254                         if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
00255                             $status->fatal( 'backend-fail-synced', $path );
00256                             continue;
00257                         }
00258                     }
00259                     if ( $this->syncChecks & self::CHECK_SHA1 ) {
00260                         if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
00261                             $status->fatal( 'backend-fail-synced', $path );
00262                             continue;
00263                         }
00264                     }
00265                 } else { // file is not in master
00266                     if ( $cStat ) { // file should not exist
00267                         $status->fatal( 'backend-fail-synced', $path );
00268                     }
00269                 }
00270             }
00271         }
00272 
00273         return $status;
00274     }
00275 
00282     public function accessibilityCheck( array $paths ) {
00283         $status = Status::newGood();
00284         if ( count( $this->backends ) <= 1 ) {
00285             return $status; // skip checks
00286         }
00287 
00288         foreach ( $paths as $path ) {
00289             foreach ( $this->backends as $backend ) {
00290                 $realPath = $this->substPaths( $path, $backend );
00291                 if ( !$backend->isPathUsableInternal( $realPath ) ) {
00292                     $status->fatal( 'backend-fail-usable', $path );
00293                 }
00294             }
00295         }
00296 
00297         return $status;
00298     }
00299 
00307     public function resyncFiles( array $paths ) {
00308         $status = Status::newGood();
00309 
00310         $mBackend = $this->backends[$this->masterIndex];
00311         foreach ( $paths as $path ) {
00312             $mPath = $this->substPaths( $path, $mBackend );
00313             $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath, 'latest' => true ) );
00314             $mStat = $mBackend->getFileStat( array( 'src' => $mPath, 'latest' => true ) );
00315             if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
00316                 $status->fatal( 'backend-fail-internal', $this->name );
00317                 continue; // file is not available on the master backend...
00318             }
00319             // Check of all clone backends agree with the master...
00320             foreach ( $this->backends as $index => $cBackend ) {
00321                 if ( $index === $this->masterIndex ) {
00322                     continue; // master
00323                 }
00324                 $cPath = $this->substPaths( $path, $cBackend );
00325                 $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath, 'latest' => true ) );
00326                 $cStat = $cBackend->getFileStat( array( 'src' => $cPath, 'latest' => true ) );
00327                 if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
00328                     $status->fatal( 'backend-fail-internal', $cBackend->getName() );
00329                     continue; // file is not available on the clone backend...
00330                 }
00331                 if ( $mSha1 === $cSha1 ) {
00332                     // already synced; nothing to do
00333                 } elseif ( $mSha1 !== false ) { // file is in master
00334                     if ( $this->autoResync === 'conservative'
00335                         && $cStat && $cStat['mtime'] > $mStat['mtime']
00336                     ) {
00337                         $status->fatal( 'backend-fail-synced', $path );
00338                         continue; // don't rollback data
00339                     }
00340                     $fsFile = $mBackend->getLocalReference(
00341                         array( 'src' => $mPath, 'latest' => true ) );
00342                     $status->merge( $cBackend->quickStore(
00343                         array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
00344                     ) );
00345                 } elseif ( $mStat === false ) { // file is not in master
00346                     if ( $this->autoResync === 'conservative' ) {
00347                         $status->fatal( 'backend-fail-synced', $path );
00348                         continue; // don't delete data
00349                     }
00350                     $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
00351                 }
00352             }
00353         }
00354 
00355         return $status;
00356     }
00357 
00364     protected function fileStoragePathsForOps( array $ops ) {
00365         $paths = array();
00366         foreach ( $ops as $op ) {
00367             if ( isset( $op['src'] ) ) {
00368                 // For things like copy/move/delete with "ignoreMissingSource" and there
00369                 // is no source file, nothing should happen and there should be no errors.
00370                 if ( empty( $op['ignoreMissingSource'] )
00371                     || $this->fileExists( array( 'src' => $op['src'] ) )
00372                 ) {
00373                     $paths[] = $op['src'];
00374                 }
00375             }
00376             if ( isset( $op['srcs'] ) ) {
00377                 $paths = array_merge( $paths, $op['srcs'] );
00378             }
00379             if ( isset( $op['dst'] ) ) {
00380                 $paths[] = $op['dst'];
00381             }
00382         }
00383 
00384         return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
00385     }
00386 
00395     protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
00396         $newOps = array(); // operations
00397         foreach ( $ops as $op ) {
00398             $newOp = $op; // operation
00399             foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
00400                 if ( isset( $newOp[$par] ) ) { // string or array
00401                     $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
00402                 }
00403             }
00404             $newOps[] = $newOp;
00405         }
00406 
00407         return $newOps;
00408     }
00409 
00417     protected function substOpPaths( array $ops, FileBackendStore $backend ) {
00418         $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
00419 
00420         return $newOps[0];
00421     }
00422 
00430     protected function substPaths( $paths, FileBackendStore $backend ) {
00431         return preg_replace(
00432             '!^mwstore://' . preg_quote( $this->name ) . '/!',
00433             StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
00434             $paths // string or array
00435         );
00436     }
00437 
00444     protected function unsubstPaths( $paths ) {
00445         return preg_replace(
00446             '!^mwstore://([^/]+)!',
00447             StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
00448             $paths // string or array
00449         );
00450     }
00451 
00452     protected function doQuickOperationsInternal( array $ops ) {
00453         $status = Status::newGood();
00454         // Do the operations on the master backend; setting Status fields...
00455         $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
00456         $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
00457         $status->merge( $masterStatus );
00458         // Propagate the operations to the clone backends...
00459         if ( !$this->noPushQuickOps ) {
00460             foreach ( $this->backends as $index => $backend ) {
00461                 if ( $index !== $this->masterIndex ) { // not done already
00462                     $realOps = $this->substOpBatchPaths( $ops, $backend );
00463                     $status->merge( $backend->doQuickOperations( $realOps ) );
00464                 }
00465             }
00466         }
00467         // Make 'success', 'successCount', and 'failCount' fields reflect
00468         // the overall operation, rather than all the batches for each backend.
00469         // Do this by only using success values from the master backend's batch.
00470         $status->success = $masterStatus->success;
00471         $status->successCount = $masterStatus->successCount;
00472         $status->failCount = $masterStatus->failCount;
00473 
00474         return $status;
00475     }
00476 
00481     protected function replicateContainerDirChanges( $path ) {
00482         list( , $shortCont, ) = self::splitStoragePath( $path );
00483 
00484         return !in_array( $shortCont, $this->noPushDirConts );
00485     }
00486 
00487     protected function doPrepare( array $params ) {
00488         $status = Status::newGood();
00489         $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00490         foreach ( $this->backends as $index => $backend ) {
00491             if ( $replicate || $index == $this->masterIndex ) {
00492                 $realParams = $this->substOpPaths( $params, $backend );
00493                 $status->merge( $backend->doPrepare( $realParams ) );
00494             }
00495         }
00496 
00497         return $status;
00498     }
00499 
00500     protected function doSecure( array $params ) {
00501         $status = Status::newGood();
00502         $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00503         foreach ( $this->backends as $index => $backend ) {
00504             if ( $replicate || $index == $this->masterIndex ) {
00505                 $realParams = $this->substOpPaths( $params, $backend );
00506                 $status->merge( $backend->doSecure( $realParams ) );
00507             }
00508         }
00509 
00510         return $status;
00511     }
00512 
00513     protected function doPublish( array $params ) {
00514         $status = Status::newGood();
00515         $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00516         foreach ( $this->backends as $index => $backend ) {
00517             if ( $replicate || $index == $this->masterIndex ) {
00518                 $realParams = $this->substOpPaths( $params, $backend );
00519                 $status->merge( $backend->doPublish( $realParams ) );
00520             }
00521         }
00522 
00523         return $status;
00524     }
00525 
00526     protected function doClean( array $params ) {
00527         $status = Status::newGood();
00528         $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00529         foreach ( $this->backends as $index => $backend ) {
00530             if ( $replicate || $index == $this->masterIndex ) {
00531                 $realParams = $this->substOpPaths( $params, $backend );
00532                 $status->merge( $backend->doClean( $realParams ) );
00533             }
00534         }
00535 
00536         return $status;
00537     }
00538 
00539     public function concatenate( array $params ) {
00540         // We are writing to an FS file, so we don't need to do this per-backend
00541         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00542 
00543         return $this->backends[$this->masterIndex]->concatenate( $realParams );
00544     }
00545 
00546     public function fileExists( array $params ) {
00547         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00548 
00549         return $this->backends[$this->masterIndex]->fileExists( $realParams );
00550     }
00551 
00552     public function getFileTimestamp( array $params ) {
00553         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00554 
00555         return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
00556     }
00557 
00558     public function getFileSize( array $params ) {
00559         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00560 
00561         return $this->backends[$this->masterIndex]->getFileSize( $realParams );
00562     }
00563 
00564     public function getFileStat( array $params ) {
00565         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00566 
00567         return $this->backends[$this->masterIndex]->getFileStat( $realParams );
00568     }
00569 
00570     public function getFileXAttributes( array $params ) {
00571         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00572 
00573         return $this->backends[$this->masterIndex]->getFileXAttributes( $realParams );
00574     }
00575 
00576     public function getFileContentsMulti( array $params ) {
00577         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00578         $contentsM = $this->backends[$this->masterIndex]->getFileContentsMulti( $realParams );
00579 
00580         $contents = array(); // (path => FSFile) mapping using the proxy backend's name
00581         foreach ( $contentsM as $path => $data ) {
00582             $contents[$this->unsubstPaths( $path )] = $data;
00583         }
00584 
00585         return $contents;
00586     }
00587 
00588     public function getFileSha1Base36( array $params ) {
00589         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00590 
00591         return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
00592     }
00593 
00594     public function getFileProps( array $params ) {
00595         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00596 
00597         return $this->backends[$this->masterIndex]->getFileProps( $realParams );
00598     }
00599 
00600     public function streamFile( array $params ) {
00601         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00602 
00603         return $this->backends[$this->masterIndex]->streamFile( $realParams );
00604     }
00605 
00606     public function getLocalReferenceMulti( array $params ) {
00607         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00608         $fsFilesM = $this->backends[$this->masterIndex]->getLocalReferenceMulti( $realParams );
00609 
00610         $fsFiles = array(); // (path => FSFile) mapping using the proxy backend's name
00611         foreach ( $fsFilesM as $path => $fsFile ) {
00612             $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
00613         }
00614 
00615         return $fsFiles;
00616     }
00617 
00618     public function getLocalCopyMulti( array $params ) {
00619         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00620         $tempFilesM = $this->backends[$this->masterIndex]->getLocalCopyMulti( $realParams );
00621 
00622         $tempFiles = array(); // (path => TempFSFile) mapping using the proxy backend's name
00623         foreach ( $tempFilesM as $path => $tempFile ) {
00624             $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
00625         }
00626 
00627         return $tempFiles;
00628     }
00629 
00630     public function getFileHttpUrl( array $params ) {
00631         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00632 
00633         return $this->backends[$this->masterIndex]->getFileHttpUrl( $realParams );
00634     }
00635 
00636     public function directoryExists( array $params ) {
00637         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00638 
00639         return $this->backends[$this->masterIndex]->directoryExists( $realParams );
00640     }
00641 
00642     public function getDirectoryList( array $params ) {
00643         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00644 
00645         return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
00646     }
00647 
00648     public function getFileList( array $params ) {
00649         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00650 
00651         return $this->backends[$this->masterIndex]->getFileList( $realParams );
00652     }
00653 
00654     public function getFeatures() {
00655         return $this->backends[$this->masterIndex]->getFeatures();
00656     }
00657 
00658     public function clearCache( array $paths = null ) {
00659         foreach ( $this->backends as $backend ) {
00660             $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
00661             $backend->clearCache( $realPaths );
00662         }
00663     }
00664 
00665     public function preloadCache( array $paths ) {
00666         $realPaths = $this->substPaths( $paths, $this->backends[$this->masterIndex] );
00667         $this->backends[$this->masterIndex]->preloadCache( $realPaths );
00668     }
00669 
00670     public function preloadFileStat( array $params ) {
00671         $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00672         return $this->backends[$this->masterIndex]->preloadFileStat( $realParams );
00673     }
00674 
00675     public function getScopedLocksForOps( array $ops, Status $status ) {
00676         $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
00677         $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps );
00678         // Get the paths to lock from the master backend
00679         $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
00680         // Get the paths under the proxy backend's name
00681         $pbPaths = array(
00682             LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
00683             LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
00684         );
00685 
00686         // Actually acquire the locks
00687         return array( $this->getScopedFileLocks( $pbPaths, 'mixed', $status ) );
00688     }
00689 }