Merge "[FileBackend] Added getScopedLocksForOps() function."
authorBrion VIBBER <brion@wikimedia.org>
Thu, 24 May 2012 21:08:40 +0000 (21:08 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 24 May 2012 21:08:40 +0000 (21:08 +0000)
1  2 
includes/filerepo/backend/FileBackend.php
includes/filerepo/backend/FileBackendMultiWrite.php
includes/filerepo/backend/FileBackendStore.php

@@@ -8,23 -8,6 +8,23 @@@
   */
  
  /**
 + * Base class for all file backends.
 + *
 + * 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
@@@ -60,9 -43,6 +60,9 @@@ abstract class FileBackend 
        protected $name; // string; unique backend name
        protected $wikiId; // string; unique wiki name
        protected $readOnly; // string; read-only explanation message
 +      protected $parallelize; // string; when to do operations in parallel
 +      protected $concurrency; // integer; how many operations can be done in parallel
 +
        /** @var LockManager */
        protected $lockManager;
        /** @var FileJournal */
         *                     Journals simply log changes to files stored in the backend.
         *     'readOnly'    : Write operations are disallowed if this is a non-empty string.
         *                     It should be an explanation for the backend being read-only.
 +       *     'parallelize' : When to do file operations in parallel (when possible).
 +       *                     Allowed values are "implicit", "explicit" and "off".
 +       *     'concurrency' : How many file operations can be done in parallel.
         *
         * @param $config Array
 +       * @throws MWException
         */
        public function __construct( array $config ) {
                $this->name = $config['name'];
                $this->readOnly = isset( $config['readOnly'] )
                        ? (string)$config['readOnly']
                        : '';
 +              $this->parallelize = isset( $config['parallelize'] )
 +                      ? (string)$config['parallelize']
 +                      : 'off';
 +              $this->concurrency = isset( $config['concurrency'] )
 +                      ? (int)$config['concurrency']
 +                      : 50;
        }
  
        /**
         *                         This has no effect unless the 'force' flag is set.
         * 'nonJournaled'        : Don't log this operation batch in the file journal.
         *                         This limits the ability of recovery scripts.
 +       * 'parallelize'         : Try to do operations in parallel when possible.
         *
         * Remarks on locking:
         * File system paths given to operations should refer to files that are
                        unset( $opts['nonLocking'] );
                        unset( $opts['allowStale'] );
                }
 +              $opts['concurrency'] = 1; // off
 +              if ( $this->parallelize === 'implicit' ) {
 +                      if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
 +                              $opts['concurrency'] = $this->concurrency;
 +                      }
 +              } elseif ( $this->parallelize === 'explicit' ) {
 +                      if ( !empty( $opts['parallelize'] ) ) {
 +                              $opts['concurrency'] = $this->concurrency;
 +                      }
 +              }
                return $this->doOperationsInternal( $ops, $opts );
        }
  
         * @return Status
         */
        final public function create( array $params, array $opts = array() ) {
 -              $params['op'] = 'create';
 -              return $this->doOperation( $params, $opts );
 +              return $this->doOperation( array( 'op' => 'create' ) + $params, $opts );
        }
  
        /**
         * @return Status
         */
        final public function store( array $params, array $opts = array() ) {
 -              $params['op'] = 'store';
 -              return $this->doOperation( $params, $opts );
 +              return $this->doOperation( array( 'op' => 'store' ) + $params, $opts );
        }
  
        /**
         * @return Status
         */
        final public function copy( array $params, array $opts = array() ) {
 -              $params['op'] = 'copy';
 -              return $this->doOperation( $params, $opts );
 +              return $this->doOperation( array( 'op' => 'copy' ) + $params, $opts );
        }
  
        /**
         * @return Status
         */
        final public function move( array $params, array $opts = array() ) {
 -              $params['op'] = 'move';
 -              return $this->doOperation( $params, $opts );
 +              return $this->doOperation( array( 'op' => 'move' ) + $params, $opts );
        }
  
        /**
         * @return Status
         */
        final public function delete( array $params, array $opts = array() ) {
 -              $params['op'] = 'delete';
 -              return $this->doOperation( $params, $opts );
 +              return $this->doOperation( array( 'op' => 'delete' ) + $params, $opts );
        }
  
 +      /**
 +       * Perform a set of independent file operations on some files.
 +       *
 +       * This does no locking, nor journaling, and possibly no stat calls.
 +       * Any destination files that already exist will be overwritten.
 +       * This should *only* be used on non-original files, like cache files.
 +       *
 +       * Supported operations and their parameters:
 +       * a) Create a new file in storage with the contents of a string
 +       *     array(
 +       *         'op'                  => 'create',
 +       *         'dst'                 => <storage path>,
 +       *         'content'             => <string of new file contents>
 +       *     )
 +       * b) Copy a file system file into storage
 +       *     array(
 +       *         'op'                  => 'store',
 +       *         'src'                 => <file system path>,
 +       *         'dst'                 => <storage path>
 +       *     )
 +       * c) Copy a file within storage
 +       *     array(
 +       *         'op'                  => 'copy',
 +       *         'src'                 => <storage path>,
 +       *         'dst'                 => <storage path>
 +       *     )
 +       * d) Move a file within storage
 +       *     array(
 +       *         'op'                  => 'move',
 +       *         'src'                 => <storage path>,
 +       *         'dst'                 => <storage path>
 +       *     )
 +       * e) Delete a file within storage
 +       *     array(
 +       *         'op'                  => 'delete',
 +       *         'src'                 => <storage path>,
 +       *         'ignoreMissingSource' => <boolean>
 +       *     )
 +       * f) Do nothing (no-op)
 +       *     array(
 +       *         'op'                  => 'null',
 +       *     )
 +       *
 +       * Boolean flags for operations (operation-specific):
 +       * 'ignoreMissingSource' : The operation will simply succeed and do
 +       *                         nothing if the source file does not exist.
 +       *
 +       * Return value:
 +       * This returns a Status, which contains all warnings and fatals that occured
 +       * during the operation. The 'failCount', 'successCount', and 'success' members
 +       * will reflect each operation attempted for the given files. The status will be
 +       * considered "OK" as long as no fatal errors occured.
 +       *
 +       * @param $ops Array Set of operations to execute
 +       * @return Status
 +       */
 +      final public function doQuickOperations( array $ops ) {
 +              if ( $this->isReadOnly() ) {
 +                      return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
 +              }
 +              foreach ( $ops as &$op ) {
 +                      $op['overwrite'] = true; // avoids RTTs in key/value stores
 +              }
 +              return $this->doQuickOperationsInternal( $ops );
 +      }
 +
 +      /**
 +       * @see FileBackend::doQuickOperations()
 +       */
 +      abstract protected function doQuickOperationsInternal( array $ops );
 +
        /**
         * Concatenate a list of storage files into a single file system file.
         * The target path should refer to a file that is already locked or
         * $params include:
         *     dir : storage directory
         *
 +       * @param $params array
         * @return bool|null Returns null on failure
         * @since 1.20
         */
         *     dir     : storage directory
         *     topOnly : only return direct child dirs of the directory
         *
 +       * @param $params array
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
         * $params include:
         *     dir : storage directory
         *
 +       * @param $params array
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
         *     dir     : storage directory
         *     topOnly : only return direct child files of the directory (@since 1.20)
         *
 +       * @param $params array
         * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileList( array $params );
         * $params include:
         *     dir : storage directory
         *
 +       * @param $params array
         * @return Traversable|Array|null Returns null on failure
         * @since 1.20
         */
                return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
        }
  
+       /**
+        * Get an array of scoped locks needed for a batch of file operations.
+        *
+        * Normally, FileBackend::doOperations() handles locking, unless
+        * the 'nonLocking' param is passed in. This function is useful if you
+        * want the files to be locked for a broader scope than just when the
+        * files are changing. For example, if you need to update DB metadata,
+        * you may want to keep the files locked until finished.
+        *
+        * @see FileBackend::doOperations()
+        *
+        * @param $ops Array List of file operations to FileBackend::doOperations()
+        * @param $status Status Status to update on lock/unlock
+        * @return Array List of ScopedFileLocks or null values
+        * @since 1.20
+        */
+       abstract public function getScopedLocksForOps( array $ops, Status $status );
        /**
         * Get the root storage path of this backend.
         * All container paths are "subdirectories" of this path.
                return "mwstore://{$this->name}";
        }
  
 +      /**
 +       * Get the file journal object for this backend
 +       *
 +       * @return FileJournal
 +       */
 +      final public function getJournal() {
 +              return $this->fileJournal;
 +      }
 +
        /**
         * Check if a given path is a "mwstore://" path.
         * This does not do any further validation or any existence checks.
@@@ -1,22 -1,5 +1,22 @@@
  <?php
  /**
 + * Proxy backend that mirrors writes to several internal backends.
 + *
 + * 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
@@@ -57,9 -40,6 +57,9 @@@ class FileBackendMultiWrite extends Fil
         *                     FileBackendStore class, but with these additional settings:
         *                         'class'         : The name of the backend class
         *                         'isMultiMaster' : This must be set for one backend.
 +       *                         'template:      : If given a backend name, this will use
 +       *                                           the config of that backend as a template.
 +       *                                           Values specified here take precedence.
         *     'syncChecks'  : Integer bitfield of internal backend sync checks to perform.
         *                     Possible bits include self::CHECK_SIZE and self::CHECK_TIME.
         *                     The checks are done before allowing any file operations.
                // Construct backends here rather than via registration
                // to keep these backends hidden from outside the proxy.
                foreach ( $config['backends'] as $index => $config ) {
 +                      if ( isset( $config['template'] ) ) {
 +                              // Config is just a modified version of a registered backend's.
 +                              // This should only be used when that config is used only be this backend.
 +                              $config = $config + FileBackendGroup::singleton()->config( $config['template'] );
 +                      }
                        $name = $config['name'];
                        if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates
                                throw new MWException( "Two or more backends defined with the name $name." );
                }
  
                // Actually attempt the operation batch...
 -              $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
 +              $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
  
                $success = array();
                $failCount = 0;
        /**
         * Same as substOpBatchPaths() but for a single operation
         *
 -       * @param $op File operation array
 +       * @param $ops array File operation array
         * @param $backend FileBackendStore
         * @return Array
         */
                );
        }
  
 +      /**
 +       * @see FileBackend::doQuickOperationsInternal()
 +       * @return Status
 +       */
 +      public function doQuickOperationsInternal( array $ops ) {
 +              // Do the operations on the master backend; setting Status fields...
 +              $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );
 +              $status = $this->backends[$this->masterIndex]->doQuickOperations( $realOps );
 +              // 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 );
 +                              $status->merge( $backend->doQuickOperations( $realOps ) );
 +                      }
 +              }
 +              return $status;
 +      }
 +
        /**
         * @see FileBackend::doPrepare()
         * @return Status
  
        /**
         * @see FileBackend::doSecure()
 +       * @param $params array
         * @return Status
         */
        protected function doSecure( array $params ) {
  
        /**
         * @see FileBackend::doClean()
 +       * @param $params array
         * @return Status
         */
        protected function doClean( array $params ) {
  
        /**
         * @see FileBackend::concatenate()
 +       * @param $params array
 +       * @return Status
         */
        public function concatenate( array $params ) {
                // We are writing to an FS file, so we don't need to do this per-backend
  
        /**
         * @see FileBackend::fileExists()
 +       * @param $params array
         */
        public function fileExists( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileTimestamp()
 +       * @param $params array
 +       * @return bool|string
         */
        public function getFileTimestamp( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileSize()
 +       * @param $params array
 +       * @return bool|int
         */
        public function getFileSize( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileStat()
 +       * @param $params array
 +       * @return Array|bool|null
         */
        public function getFileStat( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileContents()
 +       * @param $params array
 +       * @return bool|string
         */
        public function getFileContents( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileSha1Base36()
 +       * @param $params array
 +       * @return bool|string
         */
        public function getFileSha1Base36( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileProps()
 +       * @param $params array
 +       * @return Array
         */
        public function getFileProps( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::streamFile()
 +       * @param $params array
 +       * @return \Status
         */
        public function streamFile( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getLocalReference()
 +       * @param $params array
 +       * @return FSFile|null
         */
        public function getLocalReference( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getLocalCopy()
 +       * @param $params array
 +       * @return null|TempFSFile
         */
        public function getLocalCopy( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::directoryExists()
 +       * @param $params array
 +       * @return bool|null
         */
        public function directoryExists( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getSubdirectoryList()
 +       * @param $params array
 +       * @return Array|null|Traversable
         */
        public function getDirectoryList( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
  
        /**
         * @see FileBackend::getFileList()
 +       * @param $params array
 +       * @return Array|null|\Traversable
         */
        public function getFileList( array $params ) {
                $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
                        $backend->clearCache( $realPaths );
                }
        }
+       /**
+        * @see FileBackend::getScopedLocksForOps()
+        */
+       public function getScopedLocksForOps( array $ops, Status $status ) {
+               $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $ops );
+               // 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
+               $paths['sh'] = $this->unsubstPaths( $paths['sh'] );
+               $paths['ex'] = $this->unsubstPaths( $paths['ex'] );
+               return array(
+                       $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
+                       $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
+               );
+       }
  }
@@@ -1,22 -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
@@@ -90,9 -73,6 +90,9 @@@ abstract class FileBackendStore extend
         *     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
         *     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
                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'] ) );
         *     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
         * $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
         *     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
         * @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() ) {
                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
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['stat'] = $stat;
                        $this->setFileCache( $path, $stat ); // update persistent cache
 +              } else {
 +                      wfDebug( __METHOD__ . ": File $path does not exist.\n" );
                }
                wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
         */
        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.
         * @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...
                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
                $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 );
                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()
         */
         * 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 ) {
        /**
         * Get the cache key for a container
         *
 -       * @param $container Resolved container name
 +       * @param $container string Resolved container name
         * @return string
         */
        private function containerCacheKey( $container ) {
        /**
         * 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, 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 ) {
 -              for ( $attempts=1; $attempts <= 3; $attempts++ ) {
 -                      if ( $this->memCache->delete( $this->containerCacheKey( $container ) ) ) {
 -                              return; // done!
 -                      }
 +              if ( !$this->memCache->delete( $this->containerCacheKey( $container ) ) ) {
 +                      trigger_error( "Unable to delete stat cache for container $container." );
                }
 -              trigger_error( "Unable to delete stat cache for container $container." );
        }
  
        /**
        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...
  
                $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__ );
        }
        /**
         * Get the cache key for a file path
         *
 -       * @param $path Storage path
 +       * @param $path string Storage path
         * @return string
         */
        private function fileCacheKey( $path ) {
        /**
         * Set the cached stat info for a file path
         *
 -       * @param $path Storage path
 +       * @param $path string Storage path
         * @param $val mixed Information to cache
 -       * @return void
         */
        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 Storage path
 -       * @return void
 +       * @param $path string Storage path
         */
        final protected function deleteFileCache( $path ) {
 -              for ( $attempts=1; $attempts <= 3; $attempts++ ) {
 -                      if ( $this->memCache->delete( $this->fileCacheKey( $path ) ) ) {
 -                              return; // done!
 -                      }
 +              if ( !$this->memCache->delete( $this->fileCacheKey( $path ) ) ) {
 +                      trigger_error( "Unable to delete stat cache for file $path." );
                }
 -              trigger_error( "Unable to delete stat cache for file $path." );
        }
  
        /**
        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...
                        }
                }
                // Get all cache entries for these container cache keys...
 -              $values = $this->memCache->getBatch( array_keys( $pathNames ) );
 +              $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 );
 +      }
 +}
 +
  /**
   * FileBackendStore helper function to handle listings that span container shards.
   * Do not use this class from places outside of FileBackendStore.
@@@ -1681,12 -1534,6 +1693,12 @@@ abstract class FileBackendStoreShardLis
   * 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 );
        }
   * 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 );
        }