*/
/**
- * Helper class for representing operations with transaction support.
+ * FileBackend helper class for representing operations.
* Do not use this class from places outside FileBackend.
*
- * Methods called from attemptBatch() should avoid throwing exceptions at all costs.
- * FileOp objects should be lightweight in order to support large arrays in memory.
+ * Methods called from FileOpBatch::attempt() should avoid throwing
+ * exceptions at all costs. FileOp objects should be lightweight in order
+ * to support large arrays in memory and serialization.
*
* @ingroup FileBackend
* @since 1.19
protected $state = self::STATE_NEW; // integer
protected $failed = false; // boolean
+ protected $async = false; // boolean
protected $useLatest = true; // boolean
protected $batchId; // string
const STATE_CHECKED = 2;
const STATE_ATTEMPTED = 3;
- /* Timeout related parameters */
- const MAX_BATCH_SIZE = 1000;
- const TIME_LIMIT_SEC = 300; // 5 minutes
-
/**
* Build a new file operation transaction
*
* @param $batchId string
* @return void
*/
- final protected function setBatchId( $batchId ) {
+ final public function setBatchId( $batchId ) {
$this->batchId = $batchId;
}
* @param $allowStale bool
* @return void
*/
- final protected function allowStaleReads( $allowStale ) {
+ final public function allowStaleReads( $allowStale ) {
$this->useLatest = !$allowStale;
}
/**
- * Attempt to perform a series of file operations.
- * Callers are responsible for handling file locking.
+ * Get the value of the parameter with the given name
*
- * $opts is an array of options, including:
- * 'force' : Errors that would normally cause a rollback do not.
- * The remaining operations are still attempted if any fail.
- * 'allowStale' : Don't require the latest available data.
- * This can increase performance for non-critical writes.
- * This has no effect unless the 'force' flag is set.
- * 'nonJournaled' : Don't log this operation batch in the file journal.
+ * @param $name string
+ * @return mixed Returns null if the parameter is not set
+ */
+ final public function getParam( $name ) {
+ return isset( $this->params[$name] ) ? $this->params[$name] : null;
+ }
+
+ /**
+ * Check if this operation failed precheck() or attempt()
*
- * The resulting Status will be "OK" unless:
- * a) unexpected operation errors occurred (network partitions, disk full...)
- * b) significant operation errors occured and 'force' was not set
+ * @return bool
+ */
+ final public function failed() {
+ return $this->failed;
+ }
+
+ /**
+ * Get a new empty predicates array for precheck()
*
- * @param $performOps Array List of FileOp operations
- * @param $opts Array Batch operation options
- * @param $journal FileJournal Journal to log operations to
- * @return Status
+ * @return Array
*/
- final public static function attemptBatch(
- array $performOps, array $opts, FileJournal $journal
- ) {
- $status = Status::newGood();
+ final public static function newPredicates() {
+ return array( 'exists' => array(), 'sha1' => array() );
+ }
- $n = count( $performOps );
- if ( $n > self::MAX_BATCH_SIZE ) {
- $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
- return $status;
- }
+ /**
+ * Get a new empty dependency tracking array for paths read/written to
+ *
+ * @return Array
+ */
+ final public static function newDependencies() {
+ return array( 'read' => array(), 'write' => array() );
+ }
- $batchId = $journal->getTimestampedUUID();
- $allowStale = !empty( $opts['allowStale'] );
- $ignoreErrors = !empty( $opts['force'] );
- $journaled = empty( $opts['nonJournaled'] );
-
- $entries = array(); // file journal entries
- $predicates = FileOp::newPredicates(); // account for previous op in prechecks
- // Do pre-checks for each operation; abort on failure...
- foreach ( $performOps as $index => $fileOp ) {
- $fileOp->setBatchId( $batchId );
- $fileOp->allowStaleReads( $allowStale );
- $oldPredicates = $predicates;
- $subStatus = $fileOp->precheck( $predicates ); // updates $predicates
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- if ( $journaled ) { // journal log entry
- $entries = array_merge( $entries,
- self::getJournalEntries( $fileOp, $oldPredicates, $predicates ) );
- }
- } else { // operation failed?
- $status->success[$index] = false;
- ++$status->failCount;
- if ( !$ignoreErrors ) {
- return $status; // abort
- }
- }
- }
+ /**
+ * Update a dependency tracking array to account for this operation
+ *
+ * @param $deps Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @return Array
+ */
+ final public function applyDependencies( array $deps ) {
+ $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
+ $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
+ return $deps;
+ }
- // Log the operations in file journal...
- if ( count( $entries ) ) {
- $subStatus = $journal->logChangeBatch( $entries, $batchId );
- if ( !$subStatus->isOK() ) {
- return $subStatus; // abort
+ /**
+ * Check if this operation changes files listed in $paths
+ *
+ * @param $paths Array Prior path reads/writes; format of FileOp::newPredicates()
+ * @return boolean
+ */
+ final public function dependsOn( array $deps ) {
+ foreach ( $this->storagePathsChanged() as $path ) {
+ if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
+ return true; // "output" or "anti" dependency
}
}
-
- if ( $ignoreErrors ) { // treat precheck() fatals as mere warnings
- $status->setResult( true, $status->value );
- }
-
- // Attempt each operation...
- foreach ( $performOps as $index => $fileOp ) {
- if ( $fileOp->failed() ) {
- continue; // nothing to do
- }
- $subStatus = $fileOp->attempt();
- $status->merge( $subStatus );
- if ( $subStatus->isOK() ) {
- $status->success[$index] = true;
- ++$status->successCount;
- } else {
- $status->success[$index] = false;
- ++$status->failCount;
- // We can't continue (even with $ignoreErrors) as $predicates is wrong.
- // Log the remaining ops as failed for recovery...
- for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
- $performOps[$i]->logFailure( 'attempt_aborted' );
- }
- return $status; // bail out
+ foreach ( $this->storagePathsRead() as $path ) {
+ if ( isset( $deps['write'][$path] ) ) {
+ return true; // "flow" dependency
}
}
-
- return $status;
+ return false;
}
/**
- * Get the file journal entries for a single file operation
+ * Get the file journal entries for this file operation
*
- * @param $fileOp FileOp
- * @param $oPredicates Array Pre-op information about files
- * @param $nPredicates Array Post-op information about files
+ * @param $oPredicates Array Pre-op info about files (format of FileOp::newPredicates)
+ * @param $nPredicates Array Post-op info about files (format of FileOp::newPredicates)
* @return Array
*/
- final protected static function getJournalEntries(
- FileOp $fileOp, array $oPredicates, array $nPredicates
- ) {
+ final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
$nullEntries = array();
$updateEntries = array();
$deleteEntries = array();
- $pathsUsed = array_merge( $fileOp->storagePathsRead(), $fileOp->storagePathsChanged() );
+ $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
foreach ( $pathsUsed as $path ) {
$nullEntries[] = array( // assertion for recovery
'op' => 'null',
'path' => $path,
- 'newSha1' => $fileOp->fileSha1( $path, $oPredicates )
+ 'newSha1' => $this->fileSha1( $path, $oPredicates )
);
}
- foreach ( $fileOp->storagePathsChanged() as $path ) {
+ foreach ( $this->storagePathsChanged() as $path ) {
if ( $nPredicates['sha1'][$path] === false ) { // deleted
$deleteEntries[] = array(
'op' => 'delete',
);
} else { // created/updated
$updateEntries[] = array(
- 'op' => $fileOp->fileExists( $path, $oPredicates ) ? 'update' : 'create',
+ 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
'path' => $path,
'newSha1' => $nPredicates['sha1'][$path]
);
return array_merge( $nullEntries, $updateEntries, $deleteEntries );
}
- /**
- * Get the value of the parameter with the given name
- *
- * @param $name string
- * @return mixed Returns null if the parameter is not set
- */
- final public function getParam( $name ) {
- return isset( $this->params[$name] ) ? $this->params[$name] : null;
- }
-
- /**
- * Check if this operation failed precheck() or attempt()
- *
- * @return bool
- */
- final public function failed() {
- return $this->failed;
- }
-
- /**
- * Get a new empty predicates array for precheck()
- *
- * @return Array
- */
- final public static function newPredicates() {
- return array( 'exists' => array(), 'sha1' => array() );
- }
-
/**
* Check preconditions of the operation without writing anything
*
}
/**
- * Attempt the operation, backing up files as needed; this must be reversible
+ * @return Status
+ */
+ protected function doPrecheck( array &$predicates ) {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation
*
* @return Status
*/
return $status;
}
+ /**
+ * @return Status
+ */
+ protected function doAttempt() {
+ return Status::newGood();
+ }
+
+ /**
+ * Attempt the operation in the background
+ *
+ * @return Status
+ */
+ final public function attemptAsync() {
+ $this->async = true;
+ $result = $this->attempt();
+ $this->async = false;
+ return $result;
+ }
+
/**
* Get the file operation parameters
*
return array( array(), array() );
}
+ /**
+ * Adjust params to FileBackendStore internal file calls
+ *
+ * @param $params Array
+ * @return Array (required params list, optional params list)
+ */
+ protected function setFlags( array $params ) {
+ return array( 'async' => $this->async ) + $params;
+ }
+
/**
* Get a list of storage paths read from for this operation
*
* @return Array
*/
- public function storagePathsRead() {
- return array();
+ final public function storagePathsRead() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsRead() );
}
/**
- * Get a list of storage paths written to for this operation
- *
+ * @see FileOp::storagePathsRead()
* @return Array
*/
- public function storagePathsChanged() {
+ protected function doStoragePathsRead() {
return array();
}
/**
- * @return Status
+ * Get a list of storage paths written to for this operation
+ *
+ * @return Array
*/
- protected function doPrecheck( array &$predicates ) {
- return Status::newGood();
+ final public function storagePathsChanged() {
+ return array_map( 'FileBackend::normalizeStoragePath', $this->doStoragePathsChanged() );
}
/**
- * @return Status
+ * @see FileOp::storagePathsChanged()
+ * @return Array
*/
- protected function doAttempt() {
- return Status::newGood();
+ protected function doStoragePathsChanged() {
+ return array();
}
/**
}
}
+ /**
+ * Get the backend this operation is for
+ *
+ * @return FileBackendStore
+ */
+ public function getBackend() {
+ return $this->backend;
+ }
+
/**
* Log a file operation failure and preserve any temp files
*
* @param $action string
* @return void
*/
- final protected function logFailure( $action ) {
+ final public function logFailure( $action ) {
$params = $this->params;
$params['failedAction'] = $action;
try {
* overwriteSame : override any existing file at destination
*/
class StoreFileOp extends FileOp {
+ /**
+ * @return array
+ */
protected function allowedParams() {
return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
}
+ /**
+ * @param $predicates array
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists on the file system
return $status;
// Check if the source file is too big
} elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
+ $status->fatal( 'backend-fail-maxsize',
+ $this->params['dst'], $this->backend->maxFileSizeInternal() );
$status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
return $status;
// Check if a file can be placed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
return $status;
}
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// Store the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->storeInternal( $this->params ) );
+ return $this->backend->storeInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
+ /**
+ * @return bool|string
+ */
protected function getSourceSha1Base36() {
wfSuppressWarnings();
$hash = sha1_file( $this->params['src'] );
return $hash;
}
- public function storagePathsChanged() {
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
$status = Status::newGood();
// Check if the source data is too big
if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
+ $status->fatal( 'backend-fail-maxsize',
+ $this->params['dst'], $this->backend->maxFileSizeInternal() );
$status->fatal( 'backend-fail-create', $this->params['dst'] );
return $status;
// Check if a file can be placed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-create', $this->params['dst'] );
return $status;
}
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
- // Create the file at the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->createInternal( $this->params ) );
+ // Create the file at the destination
+ return $this->backend->createInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
+ /**
+ * @return bool|String
+ */
protected function getSourceSha1Base36() {
return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
* overwriteSame : override any existing file at destination
*/
class CopyFileOp extends FileOp {
+ /**
+ * @return array
+ */
protected function allowedParams() {
return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
}
+ /**
+ * @param $predicates array
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
return $status;
// Check if a file can be placed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
return $status;
}
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// Do nothing if the src/dst paths are the same
if ( $this->params['src'] !== $this->params['dst'] ) {
// Copy the file into the destination
if ( !$this->destSameAsSource ) {
- $status->merge( $this->backend->copyInternal( $this->params ) );
+ return $this->backend->copyInternal( $this->setFlags( $this->params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['dst'] );
}
}
* overwriteSame : override any existing file at destination
*/
class MoveFileOp extends FileOp {
+ /**
+ * @return array
+ */
protected function allowedParams() {
return array( array( 'src', 'dst' ), array( 'overwrite', 'overwriteSame' ) );
}
+ /**
+ * @param $predicates array
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
return $status;
// Check if a file can be placed at the destination
} elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
+ $status->fatal( 'backend-fail-usable', $this->params['dst'] );
$status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
return $status;
}
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
// Do nothing if the src/dst paths are the same
if ( $this->params['src'] !== $this->params['dst'] ) {
if ( !$this->destSameAsSource ) {
// Move the file into the destination
- $status->merge( $this->backend->moveInternal( $this->params ) );
+ return $this->backend->moveInternal( $this->setFlags( $this->params ) );
} else {
// Just delete source as the destination needs no changes
$params = array( 'src' => $this->params['src'] );
- $status->merge( $this->backend->deleteInternal( $params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $params ) );
}
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsRead() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsRead() {
return array( $this->params['src'] );
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['src'], $this->params['dst'] );
}
}
* ignoreMissingSource : don't return an error if the file does not exist
*/
class DeleteFileOp extends FileOp {
+ /**
+ * @return array
+ */
protected function allowedParams() {
return array( array( 'src' ), array( 'ignoreMissingSource' ) );
}
protected $needsDelete = true;
+ /**
+ * @param array $predicates
+ * @return Status
+ */
protected function doPrecheck( array &$predicates ) {
$status = Status::newGood();
// Check if the source file exists
return $status; // safe to call attempt()
}
+ /**
+ * @return Status
+ */
protected function doAttempt() {
- $status = Status::newGood();
if ( $this->needsDelete ) {
// Delete the source file
- $status->merge( $this->backend->deleteInternal( $this->params ) );
+ return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
}
- return $status;
+ return Status::newGood();
}
- public function storagePathsChanged() {
+ /**
+ * @return array
+ */
+ protected function doStoragePathsChanged() {
return array( $this->params['src'] );
}
}