MediaWiki
REL1_20
|
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 }