*/
class FileBackendMultiWrite extends FileBackend {
/** @var FileBackendStore[] Prioritized list of FileBackendStore objects */
- protected $backends = array();
+ protected $backends = [];
/** @var int Index of master backend */
protected $masterIndex = -1;
/** @var int Bitfield */
protected $syncChecks = 0;
-
/** @var string|bool */
protected $autoResync = false;
+ /** @var bool */
+ protected $asyncWrites = false;
+
/* Possible internal backend consistency checks */
const CHECK_SIZE = 1;
const CHECK_TIME = 2;
* Use "conservative" to limit resyncing to copying newer master
* backend files over older (or non-existing) clone backend files.
* Cases that cannot be handled will result in operation abortion.
+ * - replication : Set to 'async' to defer file operations on the non-master backends.
+ * This will apply such updates post-send for web requests. Note that
+ * any checks from "syncChecks" are still synchronous.
*
* @param array $config
* @throws FileBackendError
$this->autoResync = isset( $config['autoResync'] )
? $config['autoResync']
: false;
+ $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async';
// Construct backends here rather than via registration
// to keep these backends hidden from outside the proxy.
- $namesUsed = array();
+ $namesUsed = [];
foreach ( $config['backends'] as $index => $config ) {
if ( isset( $config['template'] ) ) {
// Config is just a modified version of a registered backend's.
$mbe = $this->backends[$this->masterIndex]; // convenience
// Try to lock those files for the scope of this function...
+ $scopeLock = null;
if ( empty( $opts['nonLocking'] ) ) {
// Try to lock those files for the scope of this function...
/** @noinspection PhpUnusedLocalVariableInspection */
// If $ops only had one operation, this might avoid backend sync inconsistencies.
if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) {
foreach ( $this->backends as $index => $backend ) {
- if ( $index !== $this->masterIndex ) { // not done already
- $realOps = $this->substOpBatchPaths( $ops, $backend );
+ if ( $index === $this->masterIndex ) {
+ continue; // done already
+ }
+
+ $realOps = $this->substOpBatchPaths( $ops, $backend );
+ if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
+ // Bind $scopeLock to the callback to preserve locks
+ DeferredUpdates::addCallableUpdate(
+ function() use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) {
+ wfDebugLog( 'FileOperationReplication',
+ "'{$backend->getName()}' async replication; paths: " .
+ FormatJson::encode( $relevantPaths ) );
+ $backend->doOperations( $realOps, $opts );
+ }
+ );
+ } else {
+ wfDebugLog( 'FileOperationReplication',
+ "'{$backend->getName()}' sync replication; paths: " .
+ FormatJson::encode( $relevantPaths ) );
$status->merge( $backend->doOperations( $realOps, $opts ) );
}
}
$mBackend = $this->backends[$this->masterIndex];
foreach ( $paths as $path ) {
- $params = array( 'src' => $path, 'latest' => true );
+ $params = [ 'src' => $path, 'latest' => true ];
$mParams = $this->substOpPaths( $params, $mBackend );
// Stat the file on the 'master' backend
$mStat = $mBackend->getFileStat( $mParams );
$mBackend = $this->backends[$this->masterIndex];
foreach ( $paths as $path ) {
$mPath = $this->substPaths( $path, $mBackend );
- $mSha1 = $mBackend->getFileSha1Base36( array( 'src' => $mPath, 'latest' => true ) );
- $mStat = $mBackend->getFileStat( array( 'src' => $mPath, 'latest' => true ) );
+ $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] );
+ $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] );
if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity
$status->fatal( 'backend-fail-internal', $this->name );
wfDebugLog( 'FileOperation', __METHOD__
continue; // master
}
$cPath = $this->substPaths( $path, $cBackend );
- $cSha1 = $cBackend->getFileSha1Base36( array( 'src' => $cPath, 'latest' => true ) );
- $cStat = $cBackend->getFileStat( array( 'src' => $cPath, 'latest' => true ) );
+ $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] );
+ $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] );
if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity
$status->fatal( 'backend-fail-internal', $cBackend->getName() );
wfDebugLog( 'FileOperation', __METHOD__ .
continue; // don't rollback data
}
$fsFile = $mBackend->getLocalReference(
- array( 'src' => $mPath, 'latest' => true ) );
+ [ 'src' => $mPath, 'latest' => true ] );
$status->merge( $cBackend->quickStore(
- array( 'src' => $fsFile->getPath(), 'dst' => $cPath )
+ [ 'src' => $fsFile->getPath(), 'dst' => $cPath ]
) );
} elseif ( $mStat === false ) { // file is not in master
if ( $this->autoResync === 'conservative' ) {
$status->fatal( 'backend-fail-synced', $path );
continue; // don't delete data
}
- $status->merge( $cBackend->quickDelete( array( 'src' => $cPath ) ) );
+ $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) );
}
}
}
* @return array List of storage paths to files (does not include directories)
*/
protected function fileStoragePathsForOps( array $ops ) {
- $paths = array();
+ $paths = [];
foreach ( $ops as $op ) {
if ( isset( $op['src'] ) ) {
// For things like copy/move/delete with "ignoreMissingSource" and there
// is no source file, nothing should happen and there should be no errors.
if ( empty( $op['ignoreMissingSource'] )
- || $this->fileExists( array( 'src' => $op['src'] ) )
+ || $this->fileExists( [ 'src' => $op['src'] ] )
) {
$paths[] = $op['src'];
}
* @return array
*/
protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) {
- $newOps = array(); // operations
+ $newOps = []; // operations
foreach ( $ops as $op ) {
$newOp = $op; // operation
- foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
+ foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) {
if ( isset( $newOp[$par] ) ) { // string or array
$newOp[$par] = $this->substPaths( $newOp[$par], $backend );
}
* @return array
*/
protected function substOpPaths( array $ops, FileBackendStore $backend ) {
- $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
+ $newOps = $this->substOpBatchPaths( [ $ops ], $backend );
return $newOps[0];
}
);
}
+ /**
+ * @param array $ops File operations for FileBackend::doOperations()
+ * @return bool Whether there are file path sources with outside lifetime/ownership
+ */
+ protected function hasVolatileSources( array $ops ) {
+ foreach ( $ops as $op ) {
+ if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) {
+ return true; // source file might be deleted anytime after do*Operations()
+ }
+ }
+
+ return false;
+ }
+
protected function doQuickOperationsInternal( array $ops ) {
$status = Status::newGood();
// Do the operations on the master backend; setting Status fields...
$status->merge( $masterStatus );
// Propagate the operations to the clone backends...
foreach ( $this->backends as $index => $backend ) {
- if ( $index !== $this->masterIndex ) { // not done already
- $realOps = $this->substOpBatchPaths( $ops, $backend );
+ if ( $index === $this->masterIndex ) {
+ continue; // done already
+ }
+
+ $realOps = $this->substOpBatchPaths( $ops, $backend );
+ if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) {
+ DeferredUpdates::addCallableUpdate(
+ function() use ( $backend, $realOps ) {
+ $backend->doQuickOperations( $realOps );
+ }
+ );
+ } else {
$status->merge( $backend->doQuickOperations( $realOps ) );
}
}
}
protected function doPrepare( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $index => $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doPrepare( $realParams ) );
- }
-
- return $status;
+ return $this->doDirectoryOp( 'prepare', $params );
}
protected function doSecure( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $index => $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doSecure( $realParams ) );
- }
-
- return $status;
+ return $this->doDirectoryOp( 'secure', $params );
}
protected function doPublish( array $params ) {
- $status = Status::newGood();
- foreach ( $this->backends as $index => $backend ) {
- $realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doPublish( $realParams ) );
- }
-
- return $status;
+ return $this->doDirectoryOp( 'publish', $params );
}
protected function doClean( array $params ) {
+ return $this->doDirectoryOp( 'clean', $params );
+ }
+
+ /**
+ * @param string $method One of (doPrepare,doSecure,doPublish,doClean)
+ * @param array $params Method arguments
+ * @return Status
+ */
+ protected function doDirectoryOp( $method, array $params ) {
$status = Status::newGood();
+
+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
+ $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams );
+ $status->merge( $masterStatus );
+
foreach ( $this->backends as $index => $backend ) {
+ if ( $index === $this->masterIndex ) {
+ continue; // already done
+ }
+
$realParams = $this->substOpPaths( $params, $backend );
- $status->merge( $backend->doClean( $realParams ) );
+ if ( $this->asyncWrites ) {
+ DeferredUpdates::addCallableUpdate(
+ function() use ( $backend, $method, $realParams ) {
+ $backend->$method( $realParams );
+ }
+ );
+ } else {
+ $status->merge( $backend->$method( $realParams ) );
+ }
}
return $status;
$contentsM = $this->backends[$index]->getFileContentsMulti( $realParams );
- $contents = array(); // (path => FSFile) mapping using the proxy backend's name
+ $contents = []; // (path => FSFile) mapping using the proxy backend's name
foreach ( $contentsM as $path => $data ) {
$contents[$this->unsubstPaths( $path )] = $data;
}
$fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams );
- $fsFiles = array(); // (path => FSFile) mapping using the proxy backend's name
+ $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name
foreach ( $fsFilesM as $path => $fsFile ) {
$fsFiles[$this->unsubstPaths( $path )] = $fsFile;
}
$tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams );
- $tempFiles = array(); // (path => TempFSFile) mapping using the proxy backend's name
+ $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name
foreach ( $tempFilesM as $path => $tempFile ) {
$tempFiles[$this->unsubstPaths( $path )] = $tempFile;
}
// Get the paths to lock from the master backend
$paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps );
// Get the paths under the proxy backend's name
- $pbPaths = array(
+ $pbPaths = [
LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ),
LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] )
- );
+ ];
// Actually acquire the locks
return $this->getScopedFileLocks( $pbPaths, 'mixed', $status );