Merge "[FileBackend] Added getScopedLocksForOps() function."
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackendStore.php
index 803f966..f02724f 100644 (file)
@@ -1,5 +1,22 @@
 <?php
 /**
+ * Base class for all backends using particular storage medium.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup FileBackend
  * @author Aaron Schulz
@@ -73,6 +90,9 @@ abstract class FileBackendStore extends FileBackend {
         *     content       : the raw file contents
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
+        *     async         : Status will be returned immediately if supported.
+        *                     If the status is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
         *
         * @param $params Array
         * @return Status
@@ -86,6 +106,7 @@ abstract class FileBackendStore extends FileBackend {
                } else {
                        $status = $this->doCreateInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -105,6 +126,9 @@ abstract class FileBackendStore extends FileBackend {
         *     src           : source path on disk
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
+        *     async         : Status will be returned immediately if supported.
+        *                     If the status is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
         *
         * @param $params Array
         * @return Status
@@ -113,10 +137,12 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ );
                wfProfileIn( __METHOD__ . '-' . $this->name );
                if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
-                       $status = Status::newFatal( 'backend-fail-store', $params['dst'] );
+                       $status = Status::newFatal( 'backend-fail-maxsize',
+                               $params['dst'], $this->maxFileSizeInternal() );
                } else {
                        $status = $this->doStoreInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
+                       $this->deleteFileCache( $params['dst'] ); // persistent cache
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -136,6 +162,9 @@ abstract class FileBackendStore extends FileBackend {
         *     src           : source storage path
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
+        *     async         : Status will be returned immediately if supported.
+        *                     If the status is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
         *
         * @param $params Array
         * @return Status
@@ -145,6 +174,7 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doCopyInternal( $params );
                $this->clearCache( array( $params['dst'] ) );
+               $this->deleteFileCache( $params['dst'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -162,6 +192,9 @@ abstract class FileBackendStore extends FileBackend {
         * $params include:
         *     src                 : source storage path
         *     ignoreMissingSource : do nothing if the source file does not exist
+        *     async               : Status will be returned immediately if supported.
+        *                           If the status is OK, then its value field will be
+        *                           set to a FileBackendStoreOpHandle object.
         *
         * @param $params Array
         * @return Status
@@ -171,6 +204,7 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( array( $params['src'] ) );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -189,6 +223,9 @@ abstract class FileBackendStore extends FileBackend {
         *     src           : source storage path
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
+        *     async         : Status will be returned immediately if supported.
+        *                     If the status is OK, then its value field will be
+        *                     set to a FileBackendStoreOpHandle object.
         *
         * @param $params Array
         * @return Status
@@ -198,6 +235,8 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doMoveInternal( $params );
                $this->clearCache( array( $params['src'], $params['dst'] ) );
+               $this->deleteFileCache( $params['src'] ); // persistent cache
+               $this->deleteFileCache( $params['dst'] ); // persistent cache
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
@@ -208,6 +247,7 @@ abstract class FileBackendStore extends FileBackend {
         * @return Status
         */
        protected function doMoveInternal( array $params ) {
+               unset( $params['async'] ); // two steps, won't work here :)
                // Copy source to dest
                $status = $this->copyInternal( $params );
                if ( $status->isOK() ) {
@@ -218,6 +258,17 @@ abstract class FileBackendStore extends FileBackend {
                return $status;
        }
 
+       /**
+        * No-op file operation that does nothing.
+        * Do not call this function from places outside FileBackend and FileOp.
+        *
+        * @param $params Array
+        * @return Status
+        */
+       final public function nullInternal( array $params ) {
+               return Status::newGood();
+       }
+
        /**
         * @see FileBackend::concatenate()
         * @return Status
@@ -489,7 +540,10 @@ abstract class FileBackendStore extends FileBackend {
                        wfProfileOut( __METHOD__ );
                        return false; // invalid storage path
                }
-               $latest = !empty( $params['latest'] );
+               $latest = !empty( $params['latest'] ); // use latest data?
+               if ( !isset( $this->cache[$path]['stat'] ) ) {
+                       $this->primeFileCache( array( $path ) ); // check persistent cache
+               }
                if ( isset( $this->cache[$path]['stat'] ) ) {
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
@@ -506,9 +560,12 @@ abstract class FileBackendStore extends FileBackend {
                wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( is_array( $stat ) ) { // don't cache negatives
+                       $stat['latest'] = $latest;
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['stat'] = $stat;
-                       $this->cache[$path]['stat']['latest'] = $latest;
+                       $this->setFileCache( $path, $stat ); // update persistent cache
+               } else {
+                       wfDebug( __METHOD__ . ": File $path does not exist.\n" );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
@@ -711,7 +768,7 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * @see FileBackend::getDirectoryList()
-        * @return Array|null|Traversable
+        * @return Traversable|Array|null Returns null on failure
         */
        final public function getDirectoryList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -738,13 +795,13 @@ abstract class FileBackendStore extends FileBackend {
         * @param $container string Resolved container name
         * @param $dir string Resolved path relative to container
         * @param $params Array
-        * @return Traversable|Array|null
+        * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getDirectoryListInternal( $container, $dir, array $params );
 
        /**
         * @see FileBackend::getFileList()
-        * @return Array|null|Traversable
+        * @return Traversable|Array|null Returns null on failure
         */
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -771,26 +828,10 @@ abstract class FileBackendStore extends FileBackend {
         * @param $container string Resolved container name
         * @param $dir string Resolved path relative to container
         * @param $params Array
-        * @return Traversable|Array|null
+        * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
-       /**
-        * Get the list of supported operations and their corresponding FileOp classes.
-        *
-        * @return Array
-        */
-       protected function supportedOperations() {
-               return array(
-                       'store'       => 'StoreFileOp',
-                       'copy'        => 'CopyFileOp',
-                       'move'        => 'MoveFileOp',
-                       'delete'      => 'DeleteFileOp',
-                       'create'      => 'CreateFileOp',
-                       'null'        => 'NullFileOp'
-               );
-       }
-
        /**
         * Return a list of FileOp objects from a list of operations.
         * Do not call this function from places outside FileBackend.
@@ -803,7 +844,14 @@ abstract class FileBackendStore extends FileBackend {
         * @throws MWException
         */
        final public function getOperationsInternal( array $ops ) {
-               $supportedOps = $this->supportedOperations();
+               $supportedOps = array(
+                       'store'       => 'StoreFileOp',
+                       'copy'        => 'CopyFileOp',
+                       'move'        => 'MoveFileOp',
+                       'delete'      => 'DeleteFileOp',
+                       'create'      => 'CreateFileOp',
+                       'null'        => 'NullFileOp'
+               );
 
                $performOps = array(); // array of FileOp objects
                // Build up ordered array of FileOps...
@@ -846,6 +894,18 @@ abstract class FileBackendStore extends FileBackend {
                return $paths;
        }
 
+       /**
+        * @see FileBackend::getScopedLocksForOps()
+        * @return Array
+        */
+       public function getScopedLocksForOps( array $ops, Status $status ) {
+               $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
+               return array(
+                       $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
+                       $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
+               );
+       }
+
        /**
         * @see FileBackend::doOperationsInternal()
         * @return Status
@@ -875,11 +935,12 @@ abstract class FileBackendStore extends FileBackend {
                // Clear any file cache entries (after locks acquired)
                $this->clearCache();
 
-               // Load from the persistent container cache
+               // Load from the persistent file and container caches
+               $this->primeFileCache( $performOps );
                $this->primeContainerCache( $performOps );
 
                // Actually attempt the operation batch...
-               $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
+               $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
 
                // Merge errors into status fields
                $status->merge( $subStatus );
@@ -890,6 +951,105 @@ abstract class FileBackendStore extends FileBackend {
                return $status;
        }
 
+       /**
+        * @see FileBackend::doQuickOperationsInternal()
+        * @return Status
+        * @throws MWException
+        */
+       final protected function doQuickOperationsInternal( array $ops ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+               $status = Status::newGood();
+
+               $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
+               $async = ( $this->parallelize === 'implicit' );
+               $maxConcurrency = $this->concurrency; // throttle
+
+               $statuses = array(); // array of (index => Status)
+               $fileOpHandles = array(); // list of (index => handle) arrays
+               $curFileOpHandles = array(); // current handle batch
+               // Perform the sync-only ops and build up op handles for the async ops...
+               foreach ( $ops as $index => $params ) {
+                       if ( !in_array( $params['op'], $supportedOps ) ) {
+                               wfProfileOut( __METHOD__ . '-' . $this->name );
+                               wfProfileOut( __METHOD__ );
+                               throw new MWException( "Operation '{$params['op']}' is not supported." );
+                       }
+                       $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
+                       $subStatus = $this->$method( array( 'async' => $async ) + $params );
+                       if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
+                               if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
+                                       $fileOpHandles[] = $curFileOpHandles; // push this batch
+                                       $curFileOpHandles = array();
+                               }
+                               $curFileOpHandles[$index] = $subStatus->value; // keep index
+                       } else { // error or completed
+                               $statuses[$index] = $subStatus; // keep index
+                       }
+               }
+               if ( count( $curFileOpHandles ) ) {
+                       $fileOpHandles[] = $curFileOpHandles; // last batch
+               }
+               // Do all the async ops that can be done concurrently...
+               foreach ( $fileOpHandles as $fileHandleBatch ) {
+                       $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
+               }
+               // Marshall and merge all the responses...
+               foreach ( $statuses as $index => $subStatus ) {
+                       $status->merge( $subStatus );
+                       if ( $subStatus->isOK() ) {
+                               $status->success[$index] = true;
+                               ++$status->successCount;
+                       } else {
+                               $status->success[$index] = false;
+                               ++$status->failCount;
+                       }
+               }
+
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+               return $status;
+       }
+
+       /**
+        * Execute a list of FileBackendStoreOpHandle handles in parallel.
+        * The resulting Status object fields will correspond
+        * to the order in which the handles where given.
+        *
+        * @param $handles Array List of FileBackendStoreOpHandle objects
+        * @return Array Map of Status objects
+        * @throws MWException
+        */
+       final public function executeOpHandlesInternal( array $fileOpHandles ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+               foreach ( $fileOpHandles as $fileOpHandle ) {
+                       if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
+                               throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
+                       } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
+                               throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." );
+                       }
+               }
+               $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
+               foreach ( $fileOpHandles as $fileOpHandle ) {
+                       $fileOpHandle->closeResources();
+               }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+               return $res;
+       }
+
+       /**
+        * @see FileBackendStore::executeOpHandlesInternal()
+        * @return Array List of corresponding Status objects
+        */
+       protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
+               foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty
+                       throw new MWException( "This backend supports no asynchronous operations." );
+               }
+               return array();
+       }
+
        /**
         * @see FileBackend::clearCache()
         */
@@ -933,6 +1093,7 @@ abstract class FileBackendStore extends FileBackend {
         * Move a cache entry to the top (such as when accessed)
         *
         * @param $path string Storage path
+        * @return void
         */
        protected function pingCache( $path ) {
                if ( isset( $this->cache[$path] ) ) {
@@ -958,6 +1119,7 @@ abstract class FileBackendStore extends FileBackend {
         * Move a cache entry to the top (such as when accessed)
         *
         * @param $path string Storage path
+        * @return void
         */
        protected function pingExpensiveCache( $path ) {
                if ( isset( $this->expensiveCache[$path] ) ) {
@@ -1055,7 +1217,7 @@ abstract class FileBackendStore extends FileBackend {
         * Any empty suffix means the container is not sharded.
         *
         * @param $container string Container name
-        * @param $relStoragePath string Storage path relative to the container
+        * @param $relPath string Storage path relative to the container
         * @return string|null Returns null if shard could not be determined
         */
        final protected function getContainerShard( $container, $relPath ) {
@@ -1182,7 +1344,7 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Get the cache key for a container
         *
-        * @param $container Resolved container name
+        * @param $container string Resolved container name
         * @return string
         */
        private function containerCacheKey( $container ) {
@@ -1192,32 +1354,35 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Set the cached info for a container
         *
-        * @param $container Resolved container name
+        * @param $container string Resolved container name
         * @param $val mixed Information to cache
-        * @return void
         */
        final protected function setContainerCache( $container, $val ) {
-               $this->memCache->set( $this->containerCacheKey( $container ), $val, 7*86400 );
+               $this->memCache->set( $this->containerCacheKey( $container ), $val, 14*86400 );
        }
 
        /**
         * Delete the cached info for a container
         *
-        * @param $container Resolved container name
-        * @return void
+        * @param $container string Resolved container name
         */
        final protected function deleteContainerCache( $container ) {
-               $this->memCache->delete( $this->containerCacheKey( $container ) );
+               if ( !$this->memCache->delete( $this->containerCacheKey( $container ) ) ) {
+                       trigger_error( "Unable to delete stat cache for container $container." );
+               }
        }
 
        /**
         * Do a batch lookup from cache for container stats for all containers
         * used in a list of container names, storage paths, or FileOp objects.
         *
-        * @param $items Array List of storage paths or FileOps
+        * @param $items Array
         * @return void
         */
        final protected function primeContainerCache( array $items ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+
                $paths = array(); // list of storage paths
                $contNames = array(); // (cache key => resolved container name)
                // Get all the paths/containers from the items...
@@ -1241,13 +1406,16 @@ abstract class FileBackendStore extends FileBackend {
 
                $contInfo = array(); // (resolved container name => cache value)
                // Get all cache entries for these container cache keys...
-               $values = $this->memCache->getBatch( array_keys( $contNames ) );
+               $values = $this->memCache->getMulti( array_keys( $contNames ) );
                foreach ( $values as $cacheKey => $val ) {
                        $contInfo[$contNames[$cacheKey]] = $val;
                }
 
                // Populate the container process cache for the backend...
                $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
        }
 
        /**
@@ -1259,6 +1427,107 @@ abstract class FileBackendStore extends FileBackend {
         * @return void
         */
        protected function doPrimeContainerCache( array $containerInfo ) {}
+
+       /**
+        * Get the cache key for a file path
+        *
+        * @param $path string Storage path
+        * @return string
+        */
+       private function fileCacheKey( $path ) {
+               return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) );
+       }
+
+       /**
+        * Set the cached stat info for a file path
+        *
+        * @param $path string Storage path
+        * @param $val mixed Information to cache
+        */
+       final protected function setFileCache( $path, $val ) {
+               $this->memCache->set( $this->fileCacheKey( $path ), $val, 7*86400 );
+       }
+
+       /**
+        * Delete the cached stat info for a file path
+        *
+        * @param $path string Storage path
+        */
+       final protected function deleteFileCache( $path ) {
+               if ( !$this->memCache->delete( $this->fileCacheKey( $path ) ) ) {
+                       trigger_error( "Unable to delete stat cache for file $path." );
+               }
+       }
+
+       /**
+        * Do a batch lookup from cache for file stats for all paths
+        * used in a list of storage paths or FileOp objects.
+        *
+        * @param $items Array List of storage paths or FileOps
+        * @return void
+        */
+       final protected function primeFileCache( array $items ) {
+               wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
+
+               $paths = array(); // list of storage paths
+               $pathNames = array(); // (cache key => storage path)
+               // Get all the paths/containers from the items...
+               foreach ( $items as $item ) {
+                       if ( $item instanceof FileOp ) {
+                               $paths = array_merge( $paths, $item->storagePathsRead() );
+                               $paths = array_merge( $paths, $item->storagePathsChanged() );
+                       } elseif ( self::isStoragePath( $item ) ) {
+                               $paths[] = $item;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
+                       if ( $rel !== null ) { // valid path for this backend
+                               $pathNames[$this->fileCacheKey( $path )] = $path;
+                       }
+               }
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getMulti( array_keys( $pathNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       if ( is_array( $val ) ) {
+                               $this->trimCache(); // limit memory
+                               $this->cache[$pathNames[$cacheKey]]['stat'] = $val;
+                       }
+               }
+
+               wfProfileOut( __METHOD__ . '-' . $this->name );
+               wfProfileOut( __METHOD__ );
+       }
+}
+
+/**
+ * FileBackendStore helper class for performing asynchronous file operations.
+ *
+ * For example, calling FileBackendStore::createInternal() with the "async"
+ * param flag may result in a Status that contains this object as a value.
+ * This class is largely backend-specific and is mostly just "magic" to be
+ * passed to FileBackendStore::executeOpHandlesInternal().
+ */
+abstract class FileBackendStoreOpHandle {
+       /** @var Array */
+       public $params = array(); // params to caller functions
+       /** @var FileBackendStore */
+       public $backend;
+       /** @var Array */
+       public $resourcesToClose = array();
+
+       public $call; // string; name that identifies the function called
+
+       /**
+        * Close all open file handles
+        *
+        * @return void
+        */
+       public function closeResources() {
+               array_map( 'fclose', $this->resourcesToClose );
+       }
 }
 
 /**
@@ -1358,7 +1627,7 @@ abstract class FileBackendStoreShardListIterator implements Iterator {
         * @return bool
         */
        public function valid() {
-               if ( $this->iter == null ) {
+               if ( $this->iter === null ) {
                        return false; // some failure?
                } elseif ( is_array( $this->iter ) ) {
                        return ( current( $this->iter ) !== false ); // no paths can have this value
@@ -1424,6 +1693,12 @@ abstract class FileBackendStoreShardListIterator implements Iterator {
  * Iterator for listing directories
  */
 class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
+       /**
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return Array|null|Traversable
+        */
        protected function listFromShard( $container, $dir, array $params ) {
                return $this->backend->getDirectoryListInternal( $container, $dir, $params );
        }
@@ -1433,6 +1708,12 @@ class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator
  * Iterator for listing regular files
  */
 class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
+       /**
+        * @param string $container
+        * @param string $dir
+        * @param array $params
+        * @return Array|null|Traversable
+        */
        protected function listFromShard( $container, $dir, array $params ) {
                return $this->backend->getFileListInternal( $container, $dir, $params );
        }