MediaWiki  REL1_19
FileBackendMultiWrite.php
Go to the documentation of this file.
00001 <?php
00023 class FileBackendMultiWrite extends FileBackend {
00025         protected $backends = array(); // array of (backend index => backends)
00026         protected $masterIndex = -1; // integer; index of master backend
00027         protected $syncChecks = 0; // integer bitfield
00028 
00029         /* Possible internal backend consistency checks */
00030         const CHECK_SIZE = 1;
00031         const CHECK_TIME = 2;
00032 
00046         public function __construct( array $config ) {
00047                 parent::__construct( $config );
00048                 $namesUsed = array();
00049                 // Construct backends here rather than via registration
00050                 // to keep these backends hidden from outside the proxy.
00051                 foreach ( $config['backends'] as $index => $config ) {
00052                         $name = $config['name'];
00053                         if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
00054                                 throw new MWException( "Two or more backends defined with the name $name." );
00055                         }
00056                         $namesUsed[$name] = 1;
00057                         if ( !isset( $config['class'] ) ) {
00058                                 throw new MWException( 'No class given for a backend config.' );
00059                         }
00060                         $class = $config['class'];
00061                         $this->backends[$index] = new $class( $config );
00062                         if ( !empty( $config['isMultiMaster'] ) ) {
00063                                 if ( $this->masterIndex >= 0 ) {
00064                                         throw new MWException( 'More than one master backend defined.' );
00065                                 }
00066                                 $this->masterIndex = $index;
00067                         }
00068                 }
00069                 if ( $this->masterIndex < 0 ) { // need backends and must have a master
00070                         throw new MWException( 'No master backend defined.' );
00071                 }
00072                 $this->syncChecks = isset( $config['syncChecks'] )
00073                         ? $config['syncChecks']
00074                         : self::CHECK_SIZE;
00075         }
00076 
00080         final protected function doOperationsInternal( array $ops, array $opts ) {
00081                 $status = Status::newGood();
00082 
00083                 $performOps = array(); // list of FileOp objects
00084                 $filesRead = $filesChanged = array(); // storage paths used
00085                 // Build up a list of FileOps. The list will have all the ops
00086                 // for one backend, then all the ops for the next, and so on.
00087                 // These batches of ops are all part of a continuous array.
00088                 // Also build up a list of files read/changed...
00089                 foreach ( $this->backends as $index => $backend ) {
00090                         $backendOps = $this->substOpBatchPaths( $ops, $backend );
00091                         // Add on the operation batch for this backend
00092                         $performOps = array_merge( $performOps, $backend->getOperations( $backendOps ) );
00093                         if ( $index == 0 ) { // first batch
00094                                 // Get the files used for these operations. Each backend has a batch of
00095                                 // the same operations, so we only need to get them from the first batch.
00096                                 foreach ( $performOps as $fileOp ) {
00097                                         $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
00098                                         $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
00099                                 }
00100                                 // Get the paths under the proxy backend's name
00101                                 $filesRead = $this->unsubstPaths( $filesRead );
00102                                 $filesChanged = $this->unsubstPaths( $filesChanged );
00103                         }
00104                 }
00105 
00106                 // Try to lock those files for the scope of this function...
00107                 if ( empty( $opts['nonLocking'] ) ) {
00108                         $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
00109                         $filesLockEx = $filesChanged;
00110                         // Get a shared lock on the parent directory of each path changed
00111                         $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
00112                         // Try to lock those files for the scope of this function...
00113                         $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
00114                         $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
00115                         if ( !$status->isOK() ) {
00116                                 return $status; // abort
00117                         }
00118                 }
00119 
00120                 // Clear any cache entries (after locks acquired)
00121                 $this->clearCache();
00122 
00123                 // Do a consistency check to see if the backends agree
00124                 if ( count( $this->backends ) > 1 ) {
00125                         $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
00126                         if ( !$status->isOK() ) {
00127                                 return $status; // abort
00128                         }
00129                 }
00130 
00131                 // Actually attempt the operation batch...
00132                 $subStatus = FileOp::attemptBatch( $performOps, $opts );
00133 
00134                 $success = array();
00135                 $failCount = $successCount = 0;
00136                 // Make 'success', 'successCount', and 'failCount' fields reflect
00137                 // the overall operation, rather than all the batches for each backend.
00138                 // Do this by only using success values from the master backend's batch.
00139                 $batchStart = $this->masterIndex * count( $ops );
00140                 $batchEnd = $batchStart + count( $ops ) - 1;
00141                 for ( $i = $batchStart; $i <= $batchEnd; $i++ ) {
00142                         if ( !isset( $subStatus->success[$i] ) ) {
00143                                 break; // failed out before trying this op
00144                         } elseif ( $subStatus->success[$i] ) {
00145                                 ++$successCount;
00146                         } else {
00147                                 ++$failCount;
00148                         }
00149                         $success[] = $subStatus->success[$i];
00150                 }
00151                 $subStatus->success = $success;
00152                 $subStatus->successCount = $successCount;
00153                 $subStatus->failCount = $failCount;
00154 
00155                 // Merge errors into status fields
00156                 $status->merge( $subStatus );
00157                 $status->success = $subStatus->success; // not done in merge()
00158 
00159                 return $status;
00160         }
00161 
00168         public function consistencyCheck( array $paths ) {
00169                 $status = Status::newGood();
00170                 if ( $this->syncChecks == 0 ) {
00171                         return $status; // skip checks
00172                 }
00173 
00174                 $mBackend = $this->backends[$this->masterIndex];
00175                 foreach ( array_unique( $paths ) as $path ) {
00176                         $params = array( 'src' => $path, 'latest' => true );
00177                         // Stat the file on the 'master' backend
00178                         $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
00179                         // Check of all clone backends agree with the master...
00180                         foreach ( $this->backends as $index => $cBackend ) {
00181                                 if ( $index === $this->masterIndex ) {
00182                                         continue; // master
00183                                 }
00184                                 $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) );
00185                                 if ( $mStat ) { // file is in master
00186                                         if ( !$cStat ) { // file should exist
00187                                                 $status->fatal( 'backend-fail-synced', $path );
00188                                                 continue;
00189                                         }
00190                                         if ( $this->syncChecks & self::CHECK_SIZE ) {
00191                                                 if ( $cStat['size'] != $mStat['size'] ) { // wrong size
00192                                                         $status->fatal( 'backend-fail-synced', $path );
00193                                                         continue;
00194                                                 }
00195                                         }
00196                                         if ( $this->syncChecks & self::CHECK_TIME ) {
00197                                                 $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
00198                                                 $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
00199                                                 if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
00200                                                         $status->fatal( 'backend-fail-synced', $path );
00201                                                         continue;
00202                                                 }
00203                                         }
00204                                 } else { // file is not in master
00205                                         if ( $cStat ) { // file should not exist
00206                                                 $status->fatal( 'backend-fail-synced', $path );
00207                                         }
00208                                 }
00209                         }
00210                 }
00211 
00212                 return $status;
00213         }
00214 
00223         protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
00224                 $newOps = array(); // operations
00225                 foreach ( $ops as $op ) {
00226                         $newOp = $op; // operation
00227                         foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
00228                                 if ( isset( $newOp[$par] ) ) { // string or array
00229                                         $newOp[$par] = $this->substPaths( $newOp[$par], $backend );
00230                                 }
00231                         }
00232                         $newOps[] = $newOp;
00233                 }
00234                 return $newOps;
00235         }
00236 
00244         protected function substOpPaths( array $ops, FileBackendStore $backend ) {
00245                 $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
00246                 return $newOps[0];
00247         }
00248 
00256         protected function substPaths( $paths, FileBackendStore $backend ) {
00257                 return preg_replace(
00258                         '!^mwstore://' . preg_quote( $this->name ) . '/!',
00259                         StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ),
00260                         $paths // string or array
00261                 );
00262         }
00263 
00270         protected function unsubstPaths( $paths ) {
00271                 return preg_replace(
00272                         '!^mwstore://([^/]+)!',
00273                         StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ),
00274                         $paths // string or array
00275                 );
00276         }
00277 
00281         public function doPrepare( array $params ) {
00282                 $status = Status::newGood();
00283                 foreach ( $this->backends as $backend ) {
00284                         $realParams = $this->substOpPaths( $params, $backend );
00285                         $status->merge( $backend->doPrepare( $realParams ) );
00286                 }
00287                 return $status;
00288         }
00289 
00293         public function doSecure( array $params ) {
00294                 $status = Status::newGood();
00295                 foreach ( $this->backends as $backend ) {
00296                         $realParams = $this->substOpPaths( $params, $backend );
00297                         $status->merge( $backend->doSecure( $realParams ) );
00298                 }
00299                 return $status;
00300         }
00301 
00305         public function doClean( array $params ) {
00306                 $status = Status::newGood();
00307                 foreach ( $this->backends as $backend ) {
00308                         $realParams = $this->substOpPaths( $params, $backend );
00309                         $status->merge( $backend->doClean( $realParams ) );
00310                 }
00311                 return $status;
00312         }
00313 
00317         public function concatenate( array $params ) {
00318                 // We are writing to an FS file, so we don't need to do this per-backend
00319                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00320                 return $this->backends[$this->masterIndex]->concatenate( $realParams );
00321         }
00322 
00326         public function fileExists( array $params ) {
00327                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00328                 return $this->backends[$this->masterIndex]->fileExists( $realParams );
00329         }
00330 
00334         public function getFileTimestamp( array $params ) {
00335                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00336                 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
00337         }
00338 
00342         public function getFileSize( array $params ) {
00343                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00344                 return $this->backends[$this->masterIndex]->getFileSize( $realParams );
00345         }
00346 
00350         public function getFileStat( array $params ) {
00351                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00352                 return $this->backends[$this->masterIndex]->getFileStat( $realParams );
00353         }
00354 
00358         public function getFileContents( array $params ) {
00359                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00360                 return $this->backends[$this->masterIndex]->getFileContents( $realParams );
00361         }
00362 
00366         public function getFileSha1Base36( array $params ) {
00367                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00368                 return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
00369         }
00370 
00374         public function getFileProps( array $params ) {
00375                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00376                 return $this->backends[$this->masterIndex]->getFileProps( $realParams );
00377         }
00378 
00382         public function streamFile( array $params ) {
00383                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00384                 return $this->backends[$this->masterIndex]->streamFile( $realParams );
00385         }
00386 
00390         public function getLocalReference( array $params ) {
00391                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00392                 return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
00393         }
00394 
00398         public function getLocalCopy( array $params ) {
00399                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00400                 return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
00401         }
00402 
00406         public function getFileList( array $params ) {
00407                 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
00408                 return $this->backends[$this->masterIndex]->getFileList( $realParams );
00409         }
00410 
00414         public function clearCache( array $paths = null ) {
00415                 foreach ( $this->backends as $backend ) {
00416                         $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null;
00417                         $backend->clearCache( $realPaths );
00418                 }
00419         }
00420 }