Merge "[FileBackend] Added container stat caching to reduce RTTs to high latency...
authorBrion VIBBER <brion@wikimedia.org>
Thu, 26 Apr 2012 22:51:54 +0000 (22:51 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 26 Apr 2012 22:51:54 +0000 (22:51 +0000)
1  2 
includes/filerepo/backend/FileBackendStore.php
includes/filerepo/backend/SwiftFileBackend.php

@@@ -19,6 -19,9 +19,9 @@@
   * @since 1.19
   */
  abstract class FileBackendStore extends FileBackend {
+       /** @var BagOStuff */
+       protected $memCache;
        /** @var Array Map of paths to small (RAM/disk) cache items */
        protected $cache = array(); // (storage path => key => value)
        protected $maxCacheSize = 100; // integer; max paths with entries
  
        protected $maxFileSize = 4294967296; // integer bytes (4GiB)
  
+       /**
+        * @see FileBackend::__construct()
+        *
+        * @param $config Array
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               $this->memCache = new EmptyBagOStuff(); // disabled by default
+       }
        /**
         * Get the maximum allowable file size given backend
         * medium restrictions and basic performance constraints.
  
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+                       $this->deleteContainerCache( $fullCont ); // purge cache
                } else { // directory is on several shards
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
                        foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
                                $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                               $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
                        }
                }
  
        }
  
        /**
 -       * @copydoc FileBackend::getFileList()
 +       * @see FileBackend::directoryExists()
 +       * @return bool|null
 +       */
 +      final public function directoryExists( array $params ) {
 +              list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 +              if ( $dir === null ) {
 +                      return false; // invalid storage path
 +              }
 +              if ( $shard !== null ) { // confined to a single container/shard
 +                      return $this->doDirectoryExists( $fullCont, $dir, $params );
 +              } else { // directory is on several shards
 +                      wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 +                      list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 +                      $res = false; // response
 +                      foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
 +                              $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
 +                              if ( $exists ) {
 +                                      $res = true;
 +                                      break; // found one!
 +                              } elseif ( $exists === null ) { // error?
 +                                      $res = null; // if we don't find anything, it is indeterminate
 +                              }
 +                      }
 +                      return $res;
 +              }
 +      }
 +
 +      /**
 +       * @see FileBackendStore::directoryExists()
 +       *
 +       * @param $container string Resolved container name
 +       * @param $dir string Resolved path relative to container
 +       * @param $params Array
 +       * @return bool|null
 +       */
 +      abstract protected function doDirectoryExists( $container, $dir, array $params );
 +
 +      /**
 +       * @see FileBackend::getDirectoryList()
 +       * @return Array|null|Traversable
 +       */
 +      final public function getDirectoryList( array $params ) {
 +              list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 +              if ( $dir === null ) { // invalid storage path
 +                      return null;
 +              }
 +              if ( $shard !== null ) {
 +                      // File listing is confined to a single container/shard
 +                      return $this->getDirectoryListInternal( $fullCont, $dir, $params );
 +              } else {
 +                      wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 +                      // File listing spans multiple containers/shards
 +                      list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 +                      return new FileBackendStoreShardDirIterator( $this,
 +                              $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
 +              }
 +      }
 +
 +      /**
 +       * Do not call this function from places outside FileBackend
 +       *
 +       * @see FileBackendStore::getDirectoryList()
 +       *
 +       * @param $container string Resolved container name
 +       * @param $dir string Resolved path relative to container
 +       * @param $params Array
 +       * @return Traversable|Array|null
 +       */
 +      abstract public function getDirectoryListInternal( $container, $dir, array $params );
 +
 +      /**
 +       * @see FileBackend::getFileList()
         * @return Array|null|Traversable
         */
        final public function getFileList( array $params ) {
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        // File listing spans multiple containers/shards
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 -                      return new FileBackendStoreShardListIterator( $this,
 +                      return new FileBackendStoreShardFileIterator( $this,
                                $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
                }
        }
         * @return Array List of FileOp objects
         * @throws MWException
         */
 -      final public function getOperations( array $ops ) {
 +      final public function getOperationsInternal( array $ops ) {
                $supportedOps = $this->supportedOperations();
  
                $performOps = array(); // array of FileOp objects
                                // Append the FileOp class
                                $performOps[] = new $class( $this, $params );
                        } else {
 -                              throw new MWException( "Operation `$opName` is not supported." );
 +                              throw new MWException( "Operation '$opName' is not supported." );
                        }
                }
  
                return $performOps;
        }
  
 +      /**
 +       * Get a list of storage paths to lock for a list of operations
 +       * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
 +       * each corresponding to a list of storage paths to be locked.
 +       *
 +       * @param $performOps Array List of FileOp objects
 +       * @return Array ('sh' => list of paths, 'ex' => list of paths)
 +       */
 +      final public function getPathsToLockForOpsInternal( array $performOps ) {
 +              // Build up a list of files to lock...
 +              $paths = array( 'sh' => array(), 'ex' => array() );
 +              foreach ( $performOps as $fileOp ) {
 +                      $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
 +                      $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
 +              }
 +              // Optimization: if doing an EX lock anyway, don't also set an SH one
 +              $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
 +              // Get a shared lock on the parent directory of each path changed
 +              $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
 +
 +              return $paths;
 +      }
 +
        /**
         * @see FileBackend::doOperationsInternal()
         * @return Status
                $status = Status::newGood();
  
                // Build up a list of FileOps...
 -              $performOps = $this->getOperations( $ops );
 +              $performOps = $this->getOperationsInternal( $ops );
  
                // Acquire any locks as needed...
                if ( empty( $opts['nonLocking'] ) ) {
                        // Build up a list of files to lock...
 -                      $filesLockEx = $filesLockSh = array();
 -                      foreach ( $performOps as $fileOp ) {
 -                              $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
 -                              $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
 -                      }
 -                      // Optimization: if doing an EX lock anyway, don't also set an SH one
 -                      $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
 -                      // Get a shared lock on the parent directory of each path changed
 -                      $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
 +                      $paths = $this->getPathsToLockForOpsInternal( $performOps );
                        // Try to lock those files for the scope of this function...
 -                      $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
 -                      $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
 +                      $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
 +                      $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
                        if ( !$status->isOK() ) {
                                wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
                        }
                }
  
-               // Clear any cache entries (after locks acquired)
+               // Clear any file cache entries (after locks acquired)
                $this->clearCache();
  
+               // Load from the persistent container cache
+               $this->primeContainerCache( $performOps );
                // Actually attempt the operation batch...
                $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
  
                return ''; // no sharding
        }
  
 +      /**
 +       * Check if a storage path maps to a single shard.
 +       * Container dirs like "a", where the container shards on "x/xy",
 +       * can reside on several shards. Such paths are tricky to handle.
 +       *
 +       * @param $storagePath string Storage path
 +       * @return bool
 +       */
 +      final public function isSingleShardPathInternal( $storagePath ) {
 +              list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
 +              return ( $shard !== null );
 +      }
 +
        /**
         * Get the sharding config for a container.
         * If greater than 0, then all file storage paths within
        protected function resolveContainerPath( $container, $relStoragePath ) {
                return $relStoragePath;
        }
+       /**
+        * Get the cache key for a container
+        *
+        * @param $container Resolved container name
+        * @return string
+        */
+       private function containerCacheKey( $container ) {
+               return wfMemcKey( 'backend', $this->getName(), 'container', $container );
+       }
+       /**
+        * Set the cached info for a container
+        *
+        * @param $container 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 );
+       }
+       /**
+        * Delete the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @return void
+        */
+       final protected function deleteContainerCache( $container ) {
+               $this->memCache->delete( $this->containerCacheKey( $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
+        * @return void
+        */
+       final protected function primeContainerCache( array $items ) {
+               $paths = array(); // list of storage paths
+               $contNames = array(); // (cache key => resolved container name)
+               // 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;
+                       } elseif ( is_string( $item ) ) { // full container name
+                               $contNames[$this->containerCacheKey( $item )] = $item;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+                       if ( $fullCont !== null ) { // valid path for this backend
+                               $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+                       }
+               }
+               $contInfo = array(); // (resolved container name => cache value)
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getBatch( 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' ) );
+       }
+       /**
+        * Fill the backend-specific process cache given an array of
+        * resolved container names and their corresponding cached info.
+        * Only containers that actually exist should appear in the map.
+        *
+        * @param $containerInfo Array Map of resolved container names to cached info
+        * @return void
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {}
  }
  
  /**
 - * FileBackendStore helper function to handle file listings that span container shards.
 + * FileBackendStore helper function to handle listings that span container shards.
   * Do not use this class from places outside of FileBackendStore.
   *
   * @ingroup FileBackend
   */
 -class FileBackendStoreShardListIterator implements Iterator {
 -      /* @var FileBackendStore */
 +abstract class FileBackendStoreShardListIterator implements Iterator {
 +      /** @var FileBackendStore */
        protected $backend;
 -      /* @var Array */
 +      /** @var Array */
        protected $params;
 -      /* @var Array */
 +      /** @var Array */
        protected $shardSuffixes;
 -      protected $container; // string
 -      protected $directory; // string
 +      protected $container; // string; full container name
 +      protected $directory; // string; resolved relative path
  
 -      /* @var Traversable */
 +      /** @var Traversable */
        protected $iter;
        protected $curShard = 0; // integer
        protected $pos = 0; // integer
  
 +      /** @var Array */
 +      protected $multiShardPaths = array(); // (rel path => 1)
 +
        /**
         * @param $backend FileBackendStore
         * @param $container string Full storage container name
                } else {
                        $this->iter->next();
                }
 +              // Filter out items that we already listed
 +              $this->filterViaNext();
                // Find the next non-empty shard if no elements are left
                $this->nextShardIteratorIfNotValid();
        }
                $this->pos = 0;
                $this->curShard = 0;
                $this->setIteratorFromCurrentShard();
 +              // Filter out items that we already listed
 +              $this->filterViaNext();
                // Find the next non-empty shard if this one has no elements
                $this->nextShardIteratorIfNotValid();
        }
                }
        }
  
 +      /**
 +       * Filter out duplicate items by advancing to the next ones
 +       */
 +      protected function filterViaNext() {
 +              while ( $this->iter->valid() ) {
 +                      $rel = $this->iter->current(); // path relative to given directory
 +                      $path = $this->params['dir'] . "/{$rel}"; // full storage path
 +                      if ( !$this->backend->isSingleShardPathInternal( $path ) ) {
 +                              // Don't keep listing paths that are on multiple shards
 +                              if ( isset( $this->multiShardPaths[$rel] ) ) {
 +                                      $this->iter->next(); // we already listed this path
 +                              } else {
 +                                      $this->multiShardPaths[$rel] = 1;
 +                                      break;
 +                              }
 +                      }
 +              }
 +      }
 +
        /**
         * If the list iterator for this container shard is out of items,
         * then move on to the next container that has items.
         */
        protected function setIteratorFromCurrentShard() {
                $suffix = $this->shardSuffixes[$this->curShard];
 -              $this->iter = $this->backend->getFileListInternal(
 +              $this->iter = $this->listFromShard(
                        "{$this->container}{$suffix}", $this->directory, $this->params );
        }
 +
 +      /**
 +       * Get the list for a given container shard
 +       *
 +       * @param $container string Resolved container name
 +       * @param $dir string Resolved path relative to container
 +       * @param $params Array
 +       * @return Traversable|Array|null
 +       */
 +      abstract protected function listFromShard( $container, $dir, array $params );
 +}
 +
 +/**
 + * Iterator for listing directories
 + */
 +class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
 +      protected function listFromShard( $container, $dir, array $params ) {
 +              return $this->backend->getDirectoryListInternal( $container, $dir, $params );
 +      }
 +}
 +
 +/**
 + * Iterator for listing regular files
 + */
 +class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
 +      protected function listFromShard( $container, $dir, array $params ) {
 +              return $this->backend->getFileListInternal( $container, $dir, $params );
 +      }
  }
@@@ -64,6 -64,8 +64,8 @@@ class SwiftFileBackend extends FileBack
                $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
                        ? $config['shardViaHashLevels']
                        : '';
+               // Cache container info to mask latency
+               $this->memCache = wfGetMainCache();
        }
  
        /**
                return $data;
        }
  
 +      /**
 +       * @see FileBackendStore::doDirectoryExists()
 +       * @return bool|null
 +       */
 +      protected function doDirectoryExists( $fullCont, $dir, array $params ) {
 +              try {
 +                      $container = $this->getContainer( $fullCont );
 +                      $prefix = ( $dir == '' ) ? null : "{$dir}/";
 +                      return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
 +              } catch ( NoSuchContainerException $e ) {
 +                      return false;
 +              } catch ( InvalidResponseException $e ) {
 +              } catch ( Exception $e ) { // some other exception?
 +                      $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
 +              }
 +
 +              return null; // error
 +      }
 +
 +      /**
 +       * @see FileBackendStore::getDirectoryListInternal()
 +       * @return SwiftFileBackendDirList
 +       */
 +      public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
 +              return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
 +      }
 +
        /**
         * @see FileBackendStore::getFileListInternal()
         * @return SwiftFileBackendFileList
         */
        public function getFileListInternal( $fullCont, $dir, array $params ) {
 -              return new SwiftFileBackendFileList( $this, $fullCont, $dir );
 +              return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
        }
  
        /**
         *
         * @param $fullCont string Resolved container name
         * @param $dir string Resolved storage directory with no trailing slash
 -       * @param $after string Storage path of file to list items after
 +       * @param $after string|null Storage path of file to list items after
         * @param $limit integer Max number of items to list
 -       * @return Array
 +       * @param $params Array Includes flag for 'topOnly'
 +       * @return Array List of relative paths of dirs directly under $dir
         */
 -      public function getFileListPageInternal( $fullCont, $dir, $after, $limit ) {
 +      public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
 +              $dirs = array();
 +
 +              try {
 +                      $container = $this->getContainer( $fullCont );
 +                      $prefix = ( $dir == '' ) ? null : "{$dir}/";
 +                      // Non-recursive: only list dirs right under $dir
 +                      if ( !empty( $params['topOnly'] ) ) {
 +                              $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
 +                              foreach ( $objects as $object ) { // files and dirs
 +                                      if ( substr( $object, -1 ) === '/' ) {
 +                                              $dirs[] = $object; // directories end in '/'
 +                                      }
 +                                      $after = $object; // update last item
 +                              }
 +                      // Recursive: list all dirs under $dir and its subdirs
 +                      } else {
 +                              // Get directory from last item of prior page
 +                              $lastDir = $this->getParentDir( $after ); // must be first page
 +                              $objects = $container->list_objects( $limit, $after, $prefix );
 +                              foreach ( $objects as $object ) { // files
 +                                      $objectDir = $this->getParentDir( $object ); // directory of object
 +                                      if ( $objectDir !== false ) { // file has a parent dir
 +                                              // Swift stores paths in UTF-8, using binary sorting.
 +                                              // See function "create_container_table" in common/db.py.
 +                                              // If a directory is not "greater" than the last one,
 +                                              // then it was already listed by the calling iterator.
 +                                              if ( $objectDir > $lastDir ) {
 +                                                      $pDir = $objectDir;
 +                                                      do { // add dir and all its parent dirs
 +                                                              $dirs[] = "{$pDir}/";
 +                                                              $pDir = $this->getParentDir( $pDir );
 +                                                      } while ( $pDir !== false // sanity
 +                                                              && $pDir > $lastDir // not done already
 +                                                              && strlen( $pDir ) > strlen( $dir ) // within $dir
 +                                                      );
 +                                              }
 +                                              $lastDir = $objectDir;
 +                                      }
 +                                      $after = $object; // update last item
 +                              }
 +                      }
 +              } catch ( NoSuchContainerException $e ) {
 +              } catch ( InvalidResponseException $e ) {
 +              } catch ( Exception $e ) { // some other exception?
 +                      $this->logException( $e, __METHOD__, array( 'cont' => $fullCont, 'dir' => $dir ) );
 +              }
 +
 +              return $dirs;
 +      }
 +
 +      protected function getParentDir( $path ) {
 +              return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
 +      }
 +
 +      /**
 +       * Do not call this function outside of SwiftFileBackendFileList
 +       *
 +       * @param $fullCont string Resolved container name
 +       * @param $dir string Resolved storage directory with no trailing slash
 +       * @param $after string|null Storage path of file to list items after
 +       * @param $limit integer Max number of items to list
 +       * @param $params Array Includes flag for 'topOnly'
 +       * @return Array List of relative paths of files under $dir
 +       */
 +      public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
                $files = array();
  
                try {
                        $container = $this->getContainer( $fullCont );
                        $prefix = ( $dir == '' ) ? null : "{$dir}/";
 -                      $files = $container->list_objects( $limit, $after, $prefix );
 +                      // Non-recursive: only list files right under $dir
 +                      if ( !empty( $params['topOnly'] ) ) { // files and dirs
 +                              $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
 +                              foreach ( $objects as $object ) {
 +                                      if ( substr( $object, -1 ) !== '/' ) {
 +                                              $files[] = $object; // directories end in '/'
 +                                      }
 +                              }
 +                      // Recursive: list all files under $dir and its subdirs
 +                      } else { // files
 +                              $files = $container->list_objects( $limit, $after, $prefix );
 +                      }
 +                      $after = end( $files ); // update last item
 +                      reset( $files ); // reset pointer
                } catch ( NoSuchContainerException $e ) {
                } catch ( InvalidResponseException $e ) {
                } catch ( Exception $e ) { // some other exception?
         * Use $reCache if the file count or byte count is needed.
         *
         * @param $container string Container name
-        * @param $reCache bool Refresh the process cache
+        * @param $bypassCache bool Bypass all caches and load from Swift
         * @return CF_Container
+        * @throws InvalidResponseException
         */
-       protected function getContainer( $container, $reCache = false ) {
+       protected function getContainer( $container, $bypassCache = false ) {
                $conn = $this->getConnection(); // Swift proxy connection
-               if ( $reCache ) {
-                       unset( $this->connContainers[$container] ); // purge cache
+               if ( $bypassCache ) { // purge cache
+                       unset( $this->connContainers[$container] );
+               } elseif ( !isset( $this->connContainers[$container] ) ) {
+                       $this->primeContainerCache( array( $container ) ); // check persistent cache
                }
                if ( !isset( $this->connContainers[$container] ) ) {
                        $contObj = $conn->get_container( $container );
                        // NoSuchContainerException not thrown: container must exist
                        if ( count( $this->connContainers ) >= $this->maxContCacheSize ) { // trim cache?
                                reset( $this->connContainers );
-                               $key = key( $this->connContainers );
-                               unset( $this->connContainers[$key] );
+                               unset( $this->connContainers[key( $this->connContainers )] );
                        }
                        $this->connContainers[$container] = $contObj; // cache it
+                       if ( !$bypassCache ) {
+                               $this->setContainerCache( $container, // update persistent cache
+                                       array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
+                               );
+                       }
                }
                return $this->connContainers[$container];
        }
         *
         * @param $container string Container name
         * @return CF_Container
+        * @throws InvalidResponseException
         */
        protected function createContainer( $container ) {
                $conn = $this->getConnection(); // Swift proxy connection
         *
         * @param $container string Container name
         * @return void
+        * @throws InvalidResponseException
         */
        protected function deleteContainer( $container ) {
                $conn = $this->getConnection(); // Swift proxy connection
                unset( $this->connContainers[$container] ); // purge cache
        }
  
+       /**
+        * @see FileBackendStore::doPrimeContainerCache()
+        * @return void
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {
+               try {
+                       $conn = $this->getConnection(); // Swift proxy connection
+                       foreach ( $containerInfo as $container => $info ) {
+                               $this->connContainers[$container] = new CF_Container(
+                                       $conn->cfs_auth,
+                                       $conn->cfs_http,
+                                       $container,
+                                       $info['count'],
+                                       $info['bytes']
+                               );
+                       }
+               } catch ( InvalidResponseException $e ) {
+               } catch ( Exception $e ) { // some other exception?
+                       $this->logException( $e, __METHOD__, array() );
+               }
+       }
        /**
         * Log an unexpected exception for this backend
         *
  }
  
  /**
 - * SwiftFileBackend helper class to page through object listings.
 + * SwiftFileBackend helper class to page through listings.
   * Swift also has a listing limit of 10,000 objects for sanity.
   * Do not use this class from places outside SwiftFileBackend.
   *
   * @ingroup FileBackend
   */
 -class SwiftFileBackendFileList implements Iterator {
 +abstract class SwiftFileBackendList implements Iterator {
        /** @var Array */
        protected $bufferIter = array();
        protected $bufferAfter = null; // string; list items *after* this path
        protected $pos = 0; // integer
 +      /** @var Array */
 +      protected $params = array();
  
        /** @var SwiftFileBackend */
        protected $backend;
 -      protected $container; //
 -      protected $dir; // string storage directory
 +      protected $container; // string; container name
 +      protected $dir; // string; storage directory
        protected $suffixStart; // integer
  
        const PAGE_SIZE = 5000; // file listing buffer size
         * @param $backend SwiftFileBackend
         * @param $fullCont string Resolved container name
         * @param $dir string Resolved directory relative to container
 +       * @param $params Array
         */
 -      public function __construct( SwiftFileBackend $backend, $fullCont, $dir ) {
 +      public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
                $this->backend = $backend;
                $this->container = $fullCont;
                $this->dir = $dir;
                } else { // dir within container
                        $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
                }
 -      }
 -
 -      /**
 -       * @see Iterator::current()
 -       * @return string|bool String or false
 -       */
 -      public function current() {
 -              return substr( current( $this->bufferIter ), $this->suffixStart );
 +              $this->params = $params;
        }
  
        /**
                // Check if there are no files left in this page and
                // advance to the next page if this page was not empty.
                if ( !$this->valid() && count( $this->bufferIter ) ) {
 -                      $this->bufferAfter = end( $this->bufferIter );
 -                      $this->bufferIter = $this->backend->getFileListPageInternal(
 -                              $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
 -                      );
 +                      $this->bufferIter = $this->pageFromList(
 +                              $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
 +                      ); // updates $this->bufferAfter
                }
        }
  
        public function rewind() {
                $this->pos = 0;
                $this->bufferAfter = null;
 -              $this->bufferIter = $this->backend->getFileListPageInternal(
 -                      $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE
 -              );
 +              $this->bufferIter = $this->pageFromList(
 +                      $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
 +              ); // updates $this->bufferAfter
        }
  
        /**
        public function valid() {
                return ( current( $this->bufferIter ) !== false ); // no paths can have this value
        }
 +
 +      /**
 +       * Get the given list portion (page)
 +       *
 +       * @param $container string Resolved container name
 +       * @param $dir string Resolved path relative to container
 +       * @param $after string|null
 +       * @param $limit integer
 +       * @param $params Array
 +       * @return Traversable|Array|null
 +       */
 +      abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
 +}
 +
 +/**
 + * Iterator for listing directories
 + */
 +class SwiftFileBackendDirList extends SwiftFileBackendList {
 +      /**
 +       * @see Iterator::current()
 +       * @return string|bool String (relative path) or false
 +       */
 +      public function current() {
 +              return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
 +      }
 +
 +      /**
 +       * @see SwiftFileBackendList::pageFromList()
 +       * @return Array
 +       */
 +      protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
 +              return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
 +      }
 +}
 +
 +/**
 + * Iterator for listing regular files
 + */
 +class SwiftFileBackendFileList extends SwiftFileBackendList {
 +      /**
 +       * @see Iterator::current()
 +       * @return string|bool String (relative path) or false
 +       */
 +      public function current() {
 +              return substr( current( $this->bufferIter ), $this->suffixStart );
 +      }
 +
 +      /**
 +       * @see SwiftFileBackendList::pageFromList()
 +       * @return Array
 +       */
 +      protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
 +              return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
 +      }
  }