MediaWiki  master
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                                 $paths[] = $op['src'];
00347                         }
00348                         if ( isset( $op['srcs'] ) ) {
00349                                 $paths = array_merge( $paths, $op['srcs'] );
00350                         }
00351                         if ( isset( $op['dst'] ) ) {
00352                                 $paths[] = $op['dst'];
00353                         }
00354                 }
00355                 return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) );
00356         }
00357 
00366         protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
00367                 $newOps = array(); // operations
00368                 foreach ( $ops as $op ) {
00369                         $newOp = $op; // operation
00370                         foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
00371                                 if ( isset( $newOp[$par] ) ) { // string or array
00372                                         $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
00373                                 }
00374                         }
00375                         $newOps[] = $newOp;
00376                 }
00377                 return $newOps;
00378         }
00379 
00387         protected function substOpPaths( array $ops, FileBackendStore $backend ) {
00388                 $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
00389                 return $newOps[0];
00390         }
00391 
00399         protected function substPaths( $paths, FileBackendStore $backend ) {
00400                 return preg_replace(
00401                         '!^mwstore://' . preg_quote( $this->name ) . '/!',
00402                         StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
00403                         $paths // string or array
00404                 );
00405         }
00406 
00413         protected function unsubstPaths( $paths ) {
00414                 return preg_replace(
00415                         '!^mwstore://([^/]+)!',
00416                         StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
00417                         $paths // string or array
00418                 );
00419         }
00420 
00425         protected function doQuickOperationsInternal( array $ops ) {
00426                 $status = Status::newGood();
00427                 // Do the operations on the master backend; setting Status fields...
00428                 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
00429                 $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
00430                 $status->merge( $masterStatus );
00431                 // Propagate the operations to the clone backends...
00432                 if ( !$this->noPushQuickOps ) {
00433                         foreach ( $this->backends as $index => $backend ) {
00434                                 if ( $index !== $this->masterIndex ) { // not done already
00435                                         $realOps = $this->substOpBatchPaths( $ops, $backend );
00436                                         $status->merge( $backend->doQuickOperations( $realOps ) );
00437                                 }
00438                         }
00439                 }
00440                 // Make 'success', 'successCount', and 'failCount' fields reflect
00441                 // the overall operation, rather than all the batches for each backend.
00442                 // Do this by only using success values from the master backend's batch.
00443                 $status->success = $masterStatus->success;
00444                 $status->successCount = $masterStatus->successCount;
00445                 $status->failCount = $masterStatus->failCount;
00446                 return $status;
00447         }
00448 
00453         protected function replicateContainerDirChanges( $path ) {
00454                 list( $b, $shortCont, $r ) = self::splitStoragePath( $path );
00455                 return !in_array( $shortCont, $this->noPushDirConts );
00456         }
00457 
00462         protected function doPrepare( array $params ) {
00463                 $status = Status::newGood();
00464                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00465                 foreach ( $this->backends as $index => $backend ) {
00466                         if ( $replicate || $index == $this->masterIndex ) {
00467                                 $realParams = $this->substOpPaths( $params, $backend );
00468                                 $status->merge( $backend->doPrepare( $realParams ) );
00469                         }
00470                 }
00471                 return $status;
00472         }
00473 
00479         protected function doSecure( array $params ) {
00480                 $status = Status::newGood();
00481                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00482                 foreach ( $this->backends as $index => $backend ) {
00483                         if ( $replicate || $index == $this->masterIndex ) {
00484                                 $realParams = $this->substOpPaths( $params, $backend );
00485                                 $status->merge( $backend->doSecure( $realParams ) );
00486                         }
00487                 }
00488                 return $status;
00489         }
00490 
00496         protected function doPublish( array $params ) {
00497                 $status = Status::newGood();
00498                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00499                 foreach ( $this->backends as $index => $backend ) {
00500                         if ( $replicate || $index == $this->masterIndex ) {
00501                                 $realParams = $this->substOpPaths( $params, $backend );
00502                                 $status->merge( $backend->doPublish( $realParams ) );
00503                         }
00504                 }
00505                 return $status;
00506         }
00507 
00513         protected function doClean( 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->doClean( $realParams ) );
00520                         }
00521                 }
00522                 return $status;
00523         }
00524 
00530         public function concatenate( array $params ) {
00531                 // We are writing to an FS file, so we don't need to do this per-backend
00532                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00533                 return $this->backends[$this->masterIndex]->concatenate( $realParams );
00534         }
00535 
00541         public function fileExists( array $params ) {
00542                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00543                 return $this->backends[$this->masterIndex]->fileExists( $realParams );
00544         }
00545 
00551         public function getFileTimestamp( array $params ) {
00552                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00553                 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
00554         }
00555 
00561         public function getFileSize( array $params ) {
00562                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00563                 return $this->backends[$this->masterIndex]->getFileSize( $realParams );
00564         }
00565 
00571         public function getFileStat( array $params ) {
00572                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00573                 return $this->backends[$this->masterIndex]->getFileStat( $realParams );
00574         }
00575 
00581         public function getFileContentsMulti( array $params ) {
00582                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00583                 $contentsM = $this->backends[$this->masterIndex]->getFileContentsMulti( $realParams );
00584 
00585                 $contents = array(); // (path => FSFile) mapping using the proxy backend's name
00586                 foreach ( $contentsM as $path => $data ) {
00587                         $contents[$this->unsubstPaths( $path )] = $data;
00588                 }
00589                 return $contents;
00590         }
00591 
00597         public function getFileSha1Base36( array $params ) {
00598                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00599                 return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
00600         }
00601 
00607         public function getFileProps( array $params ) {
00608                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00609                 return $this->backends[$this->masterIndex]->getFileProps( $realParams );
00610         }
00611 
00617         public function streamFile( array $params ) {
00618                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00619                 return $this->backends[$this->masterIndex]->streamFile( $realParams );
00620         }
00621 
00627         public function getLocalReferenceMulti( array $params ) {
00628                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00629                 $fsFilesM = $this->backends[$this->masterIndex]->getLocalReferenceMulti( $realParams );
00630 
00631                 $fsFiles = array(); // (path => FSFile) mapping using the proxy backend's name
00632                 foreach ( $fsFilesM as $path => $fsFile ) {
00633                         $fsFiles[$this->unsubstPaths( $path )] = $fsFile;
00634                 }
00635                 return $fsFiles;
00636         }
00637 
00643         public function getLocalCopyMulti( array $params ) {
00644                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00645                 $tempFilesM = $this->backends[$this->masterIndex]->getLocalCopyMulti( $realParams );
00646 
00647                 $tempFiles = array(); // (path => TempFSFile) mapping using the proxy backend's name
00648                 foreach ( $tempFilesM as $path => $tempFile ) {
00649                         $tempFiles[$this->unsubstPaths( $path )] = $tempFile;
00650                 }
00651                 return $tempFiles;
00652         }
00653 
00658         public function getFileHttpUrl( array $params ) {
00659                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00660                 return $this->backends[$this->masterIndex]->getFileHttpUrl( $realParams );
00661         }
00662 
00668         public function directoryExists( array $params ) {
00669                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00670                 return $this->backends[$this->masterIndex]->directoryExists( $realParams );
00671         }
00672 
00678         public function getDirectoryList( array $params ) {
00679                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00680                 return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
00681         }
00682 
00688         public function getFileList( array $params ) {
00689                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00690                 return $this->backends[$this->masterIndex]->getFileList( $realParams );
00691         }
00692 
00696         public function clearCache( array $paths = null ) {
00697                 foreach ( $this->backends as $backend ) {
00698                         $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
00699                         $backend->clearCache( $realPaths );
00700                 }
00701         }
00702 
00706         public function getScopedLocksForOps( array $ops, Status $status ) {
00707                 $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
00708                 // Get the paths to lock from the master backend
00709                 $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
00710                 // Get the paths under the proxy backend's name
00711                 $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
00712                 $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
00713                 return array(
00714                         $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
00715                         $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
00716                 );
00717         }
00718 }