MediaWiki  REL1_21
FileBackendMultiWrite.php
Go to the documentation of this file.
00001 <?php
00042 class FileBackendMultiWrite extends FileBackend {
00044         protected $backends = array(); // array of (backend index => backends)
00045         protected $masterIndex = -1; // integer; index of master backend
00046         protected $syncChecks = 0; // integer; bitfield
00047         protected $autoResync = false; // boolean
00048 
00050         protected $noPushDirConts = array();
00051         protected $noPushQuickOps = false; // boolean
00052 
00053         /* Possible internal backend consistency checks */
00054         const CHECK_SIZE = 1;
00055         const CHECK_TIME = 2;
00056         const CHECK_SHA1 = 4;
00057 
00084         public function __construct( array $config ) {
00085                 parent::__construct( $config );
00086                 $this->syncChecks = isset( $config['syncChecks'] )
00087                         ? $config['syncChecks']
00088                         : self::CHECK_SIZE;
00089                 $this->autoResync = !empty( $config['autoResync'] );
00090                 $this->noPushQuickOps = isset( $config['noPushQuickOps'] )
00091                         ? $config['noPushQuickOps']
00092                         : false;
00093                 $this->noPushDirConts = isset( $config['noPushDirConts'] )
00094                         ? $config['noPushDirConts']
00095                         : array();
00096                 // Construct backends here rather than via registration
00097                 // to keep these backends hidden from outside the proxy.
00098                 $namesUsed = array();
00099                 foreach ( $config['backends'] as $index => $config ) {
00100                         if ( isset( $config['template'] ) ) {
00101                                 // Config is just a modified version of a registered backend's.
00102                                 // This should only be used when that config is used only by this backend.
00103                                 $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
00104                         }
00105                         $name = $config['name'];
00106                         if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
00107                                 throw new MWException( "Two or more backends defined with the name $name." );
00108                         }
00109                         $namesUsed[$name] = 1;
00110                         // Alter certain sub-backend settings for sanity
00111                         unset( $config['readOnly'] ); // use proxy backend setting
00112                         unset( $config['fileJournal'] ); // use proxy backend journal
00113                         $config['wikiId'] = $this->wikiId; // use the proxy backend wiki ID
00114                         $config['lockManager'] = 'nullLockManager'; // lock under proxy backend
00115                         if ( !empty( $config['isMultiMaster'] ) ) {
00116                                 if ( $this->masterIndex >= 0 ) {
00117                                         throw new MWException( 'More than one master backend defined.' );
00118                                 }
00119                                 $this->masterIndex = $index; // this is the "master"
00120                                 $config['fileJournal'] = $this->fileJournal; // log under proxy backend
00121                         }
00122                         // Create sub-backend object
00123                         if ( !isset( $config['class'] ) ) {
00124                                 throw new MWException( 'No class given for a backend config.' );
00125                         }
00126                         $class = $config['class'];
00127                         $this->backends[$index] = new $class( $config );
00128                 }
00129                 if ( $this->masterIndex < 0 ) { // need backends and must have a master
00130                         throw new MWException( 'No master backend defined.' );
00131                 }
00132         }
00133 
00138         final protected function doOperationsInternal( array $ops, array $opts ) {
00139                 $status = Status::newGood();
00140 
00141                 $mbe = $this->backends[$this->masterIndex]; // convenience
00142 
00143                 // Get the paths to lock from the master backend
00144                 $realOps = $this->substOpBatchPaths( $ops, $mbe );
00145                 $paths = $mbe->getPathsToLockForOpsInternal( $mbe->getOperationsInternal( $realOps ) );
00146                 // Get the paths under the proxy backend's name
00147                 $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
00148                 $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
00149                 // Try to lock those files for the scope of this function...
00150                 if ( empty( $opts['nonLocking'] ) ) {
00151                         // Try to lock those files for the scope of this function...
00152                         $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
00153                         $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
00154                         if ( !$status->isOK() ) {
00155                                 return $status; // abort
00156                         }
00157                 }
00158                 // Clear any cache entries (after locks acquired)
00159                 $this->clearCache();
00160                 $opts['preserveCache'] = true; // only locked files are cached
00161                 // Get the list of paths to read/write...
00162                 $relevantPaths = $this->fileStoragePathsForOps( $ops );
00163                 // Check if the paths are valid and accessible on all backends...
00164                 $status->merge( $this->accessibilityCheck( $relevantPaths ) );
00165                 if ( !$status->isOK() ) {
00166                         return $status; // abort
00167                 }
00168                 // Do a consistency check to see if the backends are consistent...
00169                 $syncStatus = $this->consistencyCheck( $relevantPaths );
00170                 if ( !$syncStatus->isOK() ) {
00171                         wfDebugLog( 'FileOperation', get_class( $this ) .
00172                                 " failed sync check: " . FormatJson::encode( $relevantPaths ) );
00173                         // Try to resync the clone backends to the master on the spot...
00174                         if ( !$this->autoResync || !$this->resyncFiles( $relevantPaths )->isOK() ) {
00175                                 $status->merge( $syncStatus );
00176                                 return $status; // abort
00177                         }
00178                 }
00179                 // Actually attempt the operation batch on the master backend...
00180                 $masterStatus = $mbe->doOperations( $realOps, $opts );
00181                 $status->merge( $masterStatus );
00182                 // Propagate the operations to the clone backends if there were no unexpected errors
00183                 // and if there were either no expected errors or if the 'force' option was used.
00184                 // However, if nothing succeeded at all, then don't replicate any of the operations.
00185                 // If $ops only had one operation, this might avoid backend sync inconsistencies.
00186                 if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
00187                         foreach ( $this->backends as $index => $backend ) {
00188                                 if ( $index !== $this->masterIndex ) { // not done already
00189                                         $realOps = $this->substOpBatchPaths( $ops, $backend );
00190                                         $status->merge( $backend->doOperations( $realOps, $opts ) );
00191                                 }
00192                         }
00193                 }
00194                 // Make 'success', 'successCount', and 'failCount' fields reflect
00195                 // the overall operation, rather than all the batches for each backend.
00196                 // Do this by only using success values from the master backend's batch.
00197                 $status->success = $masterStatus->success;
00198                 $status->successCount = $masterStatus->successCount;
00199                 $status->failCount = $masterStatus->failCount;
00200 
00201                 return $status;
00202         }
00203 
00210         public function consistencyCheck( array $paths ) {
00211                 $status = Status::newGood();
00212                 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
00213                         return $status; // skip checks
00214                 }
00215 
00216                 $mBackend = $this->backends[$this->masterIndex];
00217                 foreach ( $paths as $path ) {
00218                         $params = array( 'src' => $path, 'latest' => true );
00219                         $mParams = $this->substOpPaths( $params, $mBackend );
00220                         // Stat the file on the 'master' backend
00221                         $mStat = $mBackend->getFileStat( $mParams );
00222                         if ( $this->syncChecks & self::CHECK_SHA1 ) {
00223                                 $mSha1 = $mBackend->getFileSha1Base36( $mParams );
00224                         } else {
00225                                 $mSha1 = false;
00226                         }
00227                         // Check if all clone backends agree with the master...
00228                         foreach ( $this->backends as $index => $cBackend ) {
00229                                 if ( $index === $this->masterIndex ) {
00230                                         continue; // master
00231                                 }
00232                                 $cParams = $this->substOpPaths( $params, $cBackend );
00233                                 $cStat = $cBackend->getFileStat( $cParams );
00234                                 if ( $mStat ) { // file is in master
00235                                         if ( !$cStat ) { // file should exist
00236                                                 $status->fatal( 'backend-fail-synced', $path );
00237                                                 continue;
00238                                         }
00239                                         if ( $this->syncChecks & self::CHECK_SIZE ) {
00240                                                 if ( $cStat['size'] != $mStat['size'] ) { // wrong size
00241                                                         $status->fatal( 'backend-fail-synced', $path );
00242                                                         continue;
00243                                                 }
00244                                         }
00245                                         if ( $this->syncChecks & self::CHECK_TIME ) {
00246                                                 $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
00247                                                 $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
00248                                                 if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
00249                                                         $status->fatal( 'backend-fail-synced', $path );
00250                                                         continue;
00251                                                 }
00252                                         }
00253                                         if ( $this->syncChecks & self::CHECK_SHA1 ) {
00254                                                 if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
00255                                                         $status->fatal( 'backend-fail-synced', $path );
00256                                                         continue;
00257                                                 }
00258                                         }
00259                                 } else { // file is not in master
00260                                         if ( $cStat ) { // file should not exist
00261                                                 $status->fatal( 'backend-fail-synced', $path );
00262                                         }
00263                                 }
00264                         }
00265                 }
00266 
00267                 return $status;
00268         }
00269 
00276         public function accessibilityCheck( array $paths ) {
00277                 $status = Status::newGood();
00278                 if ( count( $this->backends ) <= 1 ) {
00279                         return $status; // skip checks
00280                 }
00281 
00282                 foreach ( $paths as $path ) {
00283                         foreach ( $this->backends as $backend ) {
00284                                 $realPath = $this->substPaths( $path, $backend );
00285                                 if ( !$backend->isPathUsableInternal( $realPath ) ) {
00286                                         $status->fatal( 'backend-fail-usable', $path );
00287                                 }
00288                         }
00289                 }
00290 
00291                 return $status;
00292         }
00293 
00301         public function resyncFiles( array $paths ) {
00302                 $status = Status::newGood();
00303 
00304                 $mBackend = $this->backends[$this->masterIndex];
00305                 foreach ( $paths as $path ) {
00306                         $mPath = $this->substPaths( $path, $mBackend );
00307                         $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath ) );
00308                         $mExist = $mBackend->fileExists( array( 'src' => $mPath ) );
00309                         // Check if the master backend is available...
00310                         if ( $mExist === null ) {
00311                                 $status->fatal( 'backend-fail-internal', $this->name );
00312                         }
00313                         // Check of all clone backends agree with the master...
00314                         foreach ( $this->backends as $index => $cBackend ) {
00315                                 if ( $index === $this->masterIndex ) {
00316                                         continue; // master
00317                                 }
00318                                 $cPath = $this->substPaths( $path, $cBackend );
00319                                 $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath ) );
00320                                 if ( $mSha1 === $cSha1 ) {
00321                                         // already synced; nothing to do
00322                                 } elseif ( $mSha1 ) { // file is in master
00323                                         $fsFile = $mBackend->getLocalReference( array( 'src' => $mPath ) );
00324                                         $status->merge( $cBackend->quickStore(
00325                                                 array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
00326                                         ) );
00327                                 } elseif ( $mExist === false ) { // file is not in master
00328                                         $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
00329                                 }
00330                         }
00331                 }
00332 
00333                 return $status;
00334         }
00335 
00342         protected function fileStoragePathsForOps( array $ops ) {
00343                 $paths = array();
00344                 foreach ( $ops as $op ) {
00345                         if ( isset( $op['src'] ) ) {
00346                                 // For things like copy/move/delete with "ignoreMissingSource" and there
00347                                 // is no source file, nothing should happen and there should be no errors.
00348                                 if ( empty( $op['ignoreMissingSource'] )
00349                                         || $this->fileExists( array( 'src' => $op['src'] ) ) )
00350                                 {
00351                                         $paths[] = $op['src'];
00352                                 }
00353                         }
00354                         if ( isset( $op['srcs'] ) ) {
00355                                 $paths = array_merge( $paths, $op['srcs'] );
00356                         }
00357                         if ( isset( $op['dst'] ) ) {
00358                                 $paths[] = $op['dst'];
00359                         }
00360                 }
00361                 return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
00362         }
00363 
00372         protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
00373                 $newOps = array(); // operations
00374                 foreach ( $ops as $op ) {
00375                         $newOp = $op; // operation
00376                         foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
00377                                 if ( isset( $newOp[$par] ) ) { // string or array
00378                                         $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
00379                                 }
00380                         }
00381                         $newOps[] = $newOp;
00382                 }
00383                 return $newOps;
00384         }
00385 
00393         protected function substOpPaths( array $ops, FileBackendStore $backend ) {
00394                 $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
00395                 return $newOps[0];
00396         }
00397 
00405         protected function substPaths( $paths, FileBackendStore $backend ) {
00406                 return preg_replace(
00407                         '!^mwstore://' . preg_quote( $this->name ) . '/!',
00408                         StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
00409                         $paths // string or array
00410                 );
00411         }
00412 
00419         protected function unsubstPaths( $paths ) {
00420                 return preg_replace(
00421                         '!^mwstore://([^/]+)!',
00422                         StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
00423                         $paths // string or array
00424                 );
00425         }
00426 
00431         protected function doQuickOperationsInternal( array $ops ) {
00432                 $status = Status::newGood();
00433                 // Do the operations on the master backend; setting Status fields...
00434                 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
00435                 $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
00436                 $status->merge( $masterStatus );
00437                 // Propagate the operations to the clone backends...
00438                 if ( !$this->noPushQuickOps ) {
00439                         foreach ( $this->backends as $index => $backend ) {
00440                                 if ( $index !== $this->masterIndex ) { // not done already
00441                                         $realOps = $this->substOpBatchPaths( $ops, $backend );
00442                                         $status->merge( $backend->doQuickOperations( $realOps ) );
00443                                 }
00444                         }
00445                 }
00446                 // Make 'success', 'successCount', and 'failCount' fields reflect
00447                 // the overall operation, rather than all the batches for each backend.
00448                 // Do this by only using success values from the master backend's batch.
00449                 $status->success = $masterStatus->success;
00450                 $status->successCount = $masterStatus->successCount;
00451                 $status->failCount = $masterStatus->failCount;
00452                 return $status;
00453         }
00454 
00459         protected function replicateContainerDirChanges( $path ) {
00460                 list( , $shortCont,  ) = self::splitStoragePath( $path );
00461                 return !in_array( $shortCont, $this->noPushDirConts );
00462         }
00463 
00468         protected function doPrepare( array $params ) {
00469                 $status = Status::newGood();
00470                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00471                 foreach ( $this->backends as $index => $backend ) {
00472                         if ( $replicate || $index == $this->masterIndex ) {
00473                                 $realParams = $this->substOpPaths( $params, $backend );
00474                                 $status->merge( $backend->doPrepare( $realParams ) );
00475                         }
00476                 }
00477                 return $status;
00478         }
00479 
00485         protected function doSecure( array $params ) {
00486                 $status = Status::newGood();
00487                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00488                 foreach ( $this->backends as $index => $backend ) {
00489                         if ( $replicate || $index == $this->masterIndex ) {
00490                                 $realParams = $this->substOpPaths( $params, $backend );
00491                                 $status->merge( $backend->doSecure( $realParams ) );
00492                         }
00493                 }
00494                 return $status;
00495         }
00496 
00502         protected function doPublish( array $params ) {
00503                 $status = Status::newGood();
00504                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00505                 foreach ( $this->backends as $index => $backend ) {
00506                         if ( $replicate || $index == $this->masterIndex ) {
00507                                 $realParams = $this->substOpPaths( $params, $backend );
00508                                 $status->merge( $backend->doPublish( $realParams ) );
00509                         }
00510                 }
00511                 return $status;
00512         }
00513 
00519         protected function doClean( array $params ) {
00520                 $status = Status::newGood();
00521                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00522                 foreach ( $this->backends as $index => $backend ) {
00523                         if ( $replicate || $index == $this->masterIndex ) {
00524                                 $realParams = $this->substOpPaths( $params, $backend );
00525                                 $status->merge( $backend->doClean( $realParams ) );
00526                         }
00527                 }
00528                 return $status;
00529         }
00530 
00536         public function concatenate( array $params ) {
00537                 // We are writing to an FS file, so we don't need to do this per-backend
00538                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00539                 return $this->backends[$this->masterIndex]->concatenate( $realParams );
00540         }
00541 
00547         public function fileExists( array $params ) {
00548                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00549                 return $this->backends[$this->masterIndex]->fileExists( $realParams );
00550         }
00551 
00557         public function getFileTimestamp( array $params ) {
00558                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00559                 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
00560         }
00561 
00567         public function getFileSize( array $params ) {
00568                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00569                 return $this->backends[$this->masterIndex]->getFileSize( $realParams );
00570         }
00571 
00577         public function getFileStat( array $params ) {
00578                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00579                 return $this->backends[$this->masterIndex]->getFileStat( $realParams );
00580         }
00581 
00587         public function getFileContentsMulti( array $params ) {
00588                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00589                 $contentsM = $this->backends[$this->masterIndex]->getFileContentsMulti( $realParams );
00590 
00591                 $contents = array(); // (path => FSFile) mapping using the proxy backend's name
00592                 foreach ( $contentsM as $path => $data ) {
00593                         $contents[$this->unsubstPaths( $path )] = $data;
00594                 }
00595                 return $contents;
00596         }
00597 
00603         public function getFileSha1Base36( array $params ) {
00604                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00605                 return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
00606         }
00607 
00613         public function getFileProps( array $params ) {
00614                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00615                 return $this->backends[$this->masterIndex]->getFileProps( $realParams );
00616         }
00617 
00623         public function streamFile( array $params ) {
00624                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00625                 return $this->backends[$this->masterIndex]->streamFile( $realParams );
00626         }
00627 
00633         public function getLocalReferenceMulti( array $params ) {
00634                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00635                 $fsFilesM = $this->backends[$this->masterIndex]->getLocalReferenceMulti( $realParams );
00636 
00637                 $fsFiles = array(); // (path => FSFile) mapping using the proxy backend's name
00638                 foreach ( $fsFilesM as $path => $fsFile ) {
00639                         $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
00640                 }
00641                 return $fsFiles;
00642         }
00643 
00649         public function getLocalCopyMulti( array $params ) {
00650                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00651                 $tempFilesM = $this->backends[$this->masterIndex]->getLocalCopyMulti( $realParams );
00652 
00653                 $tempFiles = array(); // (path => TempFSFile) mapping using the proxy backend's name
00654                 foreach ( $tempFilesM as $path => $tempFile ) {
00655                         $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
00656                 }
00657                 return $tempFiles;
00658         }
00659 
00664         public function getFileHttpUrl( array $params ) {
00665                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00666                 return $this->backends[$this->masterIndex]->getFileHttpUrl( $realParams );
00667         }
00668 
00674         public function directoryExists( array $params ) {
00675                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00676                 return $this->backends[$this->masterIndex]->directoryExists( $realParams );
00677         }
00678 
00684         public function getDirectoryList( array $params ) {
00685                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00686                 return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
00687         }
00688 
00694         public function getFileList( array $params ) {
00695                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00696                 return $this->backends[$this->masterIndex]->getFileList( $realParams );
00697         }
00698 
00702         public function clearCache( array $paths = null ) {
00703                 foreach ( $this->backends as $backend ) {
00704                         $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
00705                         $backend->clearCache( $realPaths );
00706                 }
00707         }
00708 
00712         public function getScopedLocksForOps( array $ops, Status $status ) {
00713                 $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
00714                 // Get the paths to lock from the master backend
00715                 $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
00716                 // Get the paths under the proxy backend's name
00717                 $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
00718                 $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
00719                 return array(
00720                         $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
00721                         $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
00722                 );
00723         }
00724 }