MediaWiki  REL1_20
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 fatal errors.
00183                 // If $ops only had one operation, this might avoid backend inconsistencies.
00184                 // This also avoids inconsistency for expected errors (like "file already exists").
00185                 if ( !count( $masterStatus->getErrorsArray() ) ) {
00186                         foreach ( $this->backends as $index => $backend ) {
00187                                 if ( $index !== $this->masterIndex ) { // not done already
00188                                         $realOps = $this->substOpBatchPaths( $ops, $backend );
00189                                         $status->merge( $backend->doOperations( $realOps, $opts ) );
00190                                 }
00191                         }
00192                 }
00193                 // Make 'success', 'successCount', and 'failCount' fields reflect
00194                 // the overall operation, rather than all the batches for each backend.
00195                 // Do this by only using success values from the master backend's batch.
00196                 $status->success = $masterStatus->success;
00197                 $status->successCount = $masterStatus->successCount;
00198                 $status->failCount = $masterStatus->failCount;
00199 
00200                 return $status;
00201         }
00202 
00209         public function consistencyCheck( array $paths ) {
00210                 $status = Status::newGood();
00211                 if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) {
00212                         return $status; // skip checks
00213                 }
00214 
00215                 $mBackend = $this->backends[$this->masterIndex];
00216                 foreach ( $paths as $path ) {
00217                         $params = array( 'src' => $path, 'latest' => true );
00218                         $mParams = $this->substOpPaths( $params, $mBackend );
00219                         // Stat the file on the 'master' backend
00220                         $mStat = $mBackend->getFileStat( $mParams );
00221                         if ( $this->syncChecks & self::CHECK_SHA1 ) {
00222                                 $mSha1 = $mBackend->getFileSha1Base36( $mParams );
00223                         } else {
00224                                 $mSha1 = false;
00225                         }
00226                         // Check if all clone backends agree with the master...
00227                         foreach ( $this->backends as $index => $cBackend ) {
00228                                 if ( $index === $this->masterIndex ) {
00229                                         continue; // master
00230                                 }
00231                                 $cParams = $this->substOpPaths( $params, $cBackend );
00232                                 $cStat = $cBackend->getFileStat( $cParams );
00233                                 if ( $mStat ) { // file is in master
00234                                         if ( !$cStat ) { // file should exist
00235                                                 $status->fatal( 'backend-fail-synced', $path );
00236                                                 continue;
00237                                         }
00238                                         if ( $this->syncChecks & self::CHECK_SIZE ) {
00239                                                 if ( $cStat['size'] != $mStat['size'] ) { // wrong size
00240                                                         $status->fatal( 'backend-fail-synced', $path );
00241                                                         continue;
00242                                                 }
00243                                         }
00244                                         if ( $this->syncChecks & self::CHECK_TIME ) {
00245                                                 $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
00246                                                 $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
00247                                                 if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
00248                                                         $status->fatal( 'backend-fail-synced', $path );
00249                                                         continue;
00250                                                 }
00251                                         }
00252                                         if ( $this->syncChecks & self::CHECK_SHA1 ) {
00253                                                 if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1
00254                                                         $status->fatal( 'backend-fail-synced', $path );
00255                                                         continue;
00256                                                 }
00257                                         }
00258                                 } else { // file is not in master
00259                                         if ( $cStat ) { // file should not exist
00260                                                 $status->fatal( 'backend-fail-synced', $path );
00261                                         }
00262                                 }
00263                         }
00264                 }
00265 
00266                 return $status;
00267         }
00268 
00275         public function accessibilityCheck( array $paths ) {
00276                 $status = Status::newGood();
00277                 if ( count( $this->backends ) <= 1 ) {
00278                         return $status; // skip checks
00279                 }
00280 
00281                 foreach ( $paths as $path ) {
00282                         foreach ( $this->backends as $backend ) {
00283                                 $realPath = $this->substPaths( $path, $backend );
00284                                 if ( !$backend->isPathUsableInternal( $realPath ) ) {
00285                                         $status->fatal( 'backend-fail-usable', $path );
00286                                 }
00287                         }
00288                 }
00289 
00290                 return $status;
00291         }
00292 
00300         public function resyncFiles( array $paths ) {
00301                 $status = Status::newGood();
00302 
00303                 $mBackend = $this->backends[$this->masterIndex];
00304                 foreach ( $paths as $path ) {
00305                         $mPath  = $this->substPaths( $path, $mBackend );
00306                         $mSha1  = $mBackend->getFileSha1Base36( array( 'src' => $mPath ) );
00307                         $mExist = $mBackend->fileExists( array( 'src' => $mPath ) );
00308                         // Check if the master backend is available...
00309                         if ( $mExist === null ) {
00310                                 $status->fatal( 'backend-fail-internal', $this->name );
00311                         }
00312                         // Check of all clone backends agree with the master...
00313                         foreach ( $this->backends as $index => $cBackend ) {
00314                                 if ( $index === $this->masterIndex ) {
00315                                         continue; // master
00316                                 }
00317                                 $cPath = $this->substPaths( $path, $cBackend );
00318                                 $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath ) );
00319                                 if ( $mSha1 === $cSha1 ) {
00320                                         // already synced; nothing to do
00321                                 } elseif ( $mSha1 ) { // file is in master
00322                                         $fsFile = $mBackend->getLocalReference( array( 'src' => $mPath ) );
00323                                         $status->merge( $cBackend->quickStore(
00324                                                 array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
00325                                         ) );
00326                                 } elseif ( $mExist === false ) { // file is not in master
00327                                         $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
00328                                 }
00329                         }
00330                 }
00331 
00332                 return $status;
00333         }
00334 
00341         protected function fileStoragePathsForOps( array $ops ) {
00342                 $paths = array();
00343                 foreach ( $ops as $op ) {
00344                         if ( isset( $op['src'] ) ) {
00345                                 $paths[] = $op['src'];
00346                         }
00347                         if ( isset( $op['srcs'] ) ) {
00348                                 $paths = array_merge( $paths, $op['srcs'] );
00349                         }
00350                         if ( isset( $op['dst'] ) ) {
00351                                 $paths[] = $op['dst'];
00352                         }
00353                 }
00354                 return array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) );
00355         }
00356 
00365         protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
00366                 $newOps = array(); // operations
00367                 foreach ( $ops as $op ) {
00368                         $newOp = $op; // operation
00369                         foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
00370                                 if ( isset( $newOp[$par] ) ) { // string or array
00371                                         $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
00372                                 }
00373                         }
00374                         $newOps[] = $newOp;
00375                 }
00376                 return $newOps;
00377         }
00378 
00386         protected function substOpPaths( array $ops, FileBackendStore $backend ) {
00387                 $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
00388                 return $newOps[0];
00389         }
00390 
00398         protected function substPaths( $paths, FileBackendStore $backend ) {
00399                 return preg_replace(
00400                         '!^mwstore://' . preg_quote( $this->name ) . '/!',
00401                         StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
00402                         $paths // string or array
00403                 );
00404         }
00405 
00412         protected function unsubstPaths( $paths ) {
00413                 return preg_replace(
00414                         '!^mwstore://([^/]+)!',
00415                         StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
00416                         $paths // string or array
00417                 );
00418         }
00419 
00424         protected function doQuickOperationsInternal( array $ops ) {
00425                 $status = Status::newGood();
00426                 // Do the operations on the master backend; setting Status fields...
00427                 $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
00428                 $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
00429                 $status->merge( $masterStatus );
00430                 // Propagate the operations to the clone backends...
00431                 if ( !$this->noPushQuickOps ) {
00432                         foreach ( $this->backends as $index => $backend ) {
00433                                 if ( $index !== $this->masterIndex ) { // not done already
00434                                         $realOps = $this->substOpBatchPaths( $ops, $backend );
00435                                         $status->merge( $backend->doQuickOperations( $realOps ) );
00436                                 }
00437                         }
00438                 }
00439                 // Make 'success', 'successCount', and 'failCount' fields reflect
00440                 // the overall operation, rather than all the batches for each backend.
00441                 // Do this by only using success values from the master backend's batch.
00442                 $status->success = $masterStatus->success;
00443                 $status->successCount = $masterStatus->successCount;
00444                 $status->failCount = $masterStatus->failCount;
00445                 return $status;
00446         }
00447 
00452         protected function replicateContainerDirChanges( $path ) {
00453                 list( $b, $shortCont, $r ) = self::splitStoragePath( $path );
00454                 return !in_array( $shortCont, $this->noPushDirConts );
00455         }
00456 
00461         protected function doPrepare( array $params ) {
00462                 $status = Status::newGood();
00463                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00464                 foreach ( $this->backends as $index => $backend ) {
00465                         if ( $replicate || $index == $this->masterIndex ) {
00466                                 $realParams = $this->substOpPaths( $params, $backend );
00467                                 $status->merge( $backend->doPrepare( $realParams ) );
00468                         }
00469                 }
00470                 return $status;
00471         }
00472 
00478         protected function doSecure( array $params ) {
00479                 $status = Status::newGood();
00480                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00481                 foreach ( $this->backends as $index => $backend ) {
00482                         if ( $replicate || $index == $this->masterIndex ) {
00483                                 $realParams = $this->substOpPaths( $params, $backend );
00484                                 $status->merge( $backend->doSecure( $realParams ) );
00485                         }
00486                 }
00487                 return $status;
00488         }
00489 
00495         protected function doPublish( array $params ) {
00496                 $status = Status::newGood();
00497                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00498                 foreach ( $this->backends as $index => $backend ) {
00499                         if ( $replicate || $index == $this->masterIndex ) {
00500                                 $realParams = $this->substOpPaths( $params, $backend );
00501                                 $status->merge( $backend->doPublish( $realParams ) );
00502                         }
00503                 }
00504                 return $status;
00505         }
00506 
00512         protected function doClean( array $params ) {
00513                 $status = Status::newGood();
00514                 $replicate = $this->replicateContainerDirChanges( $params['dir'] );
00515                 foreach ( $this->backends as $index => $backend ) {
00516                         if ( $replicate || $index == $this->masterIndex ) {
00517                                 $realParams = $this->substOpPaths( $params, $backend );
00518                                 $status->merge( $backend->doClean( $realParams ) );
00519                         }
00520                 }
00521                 return $status;
00522         }
00523 
00529         public function concatenate( array $params ) {
00530                 // We are writing to an FS file, so we don't need to do this per-backend
00531                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00532                 return $this->backends[$this->masterIndex]->concatenate( $realParams );
00533         }
00534 
00539         public function fileExists( array $params ) {
00540                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00541                 return $this->backends[$this->masterIndex]->fileExists( $realParams );
00542         }
00543 
00549         public function getFileTimestamp( array $params ) {
00550                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00551                 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
00552         }
00553 
00559         public function getFileSize( array $params ) {
00560                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00561                 return $this->backends[$this->masterIndex]->getFileSize( $realParams );
00562         }
00563 
00569         public function getFileStat( array $params ) {
00570                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00571                 return $this->backends[$this->masterIndex]->getFileStat( $realParams );
00572         }
00573 
00579         public function getFileContents( array $params ) {
00580                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00581                 return $this->backends[$this->masterIndex]->getFileContents( $realParams );
00582         }
00583 
00589         public function getFileSha1Base36( array $params ) {
00590                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00591                 return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
00592         }
00593 
00599         public function getFileProps( array $params ) {
00600                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00601                 return $this->backends[$this->masterIndex]->getFileProps( $realParams );
00602         }
00603 
00609         public function streamFile( array $params ) {
00610                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00611                 return $this->backends[$this->masterIndex]->streamFile( $realParams );
00612         }
00613 
00619         public function getLocalReference( array $params ) {
00620                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00621                 return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
00622         }
00623 
00629         public function getLocalCopy( array $params ) {
00630                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00631                 return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
00632         }
00633 
00639         public function directoryExists( array $params ) {
00640                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00641                 return $this->backends[$this->masterIndex]->directoryExists( $realParams );
00642         }
00643 
00649         public function getDirectoryList( array $params ) {
00650                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00651                 return $this->backends[$this->masterIndex]->getDirectoryList( $realParams );
00652         }
00653 
00659         public function getFileList( array $params ) {
00660                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00661                 return $this->backends[$this->masterIndex]->getFileList( $realParams );
00662         }
00663 
00667         public function clearCache( array $paths = null ) {
00668                 foreach ( $this->backends as $backend ) {
00669                         $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
00670                         $backend->clearCache( $realPaths );
00671                 }
00672         }
00673 
00677         public function getScopedLocksForOps( array $ops, Status $status ) {
00678                 $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
00679                 // Get the paths to lock from the master backend
00680                 $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
00681                 // Get the paths under the proxy backend's name
00682                 $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
00683                 $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
00684                 return array(
00685                         $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
00686                         $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
00687                 );
00688         }
00689 }