Merge "[FileBackend] Bumped Swift auth caching TTL."
[lhc/web/wiklou.git] / includes / filebackend / SwiftFileBackend.php
index ea66bf6..960d71c 100644 (file)
@@ -47,8 +47,14 @@ class SwiftFileBackend extends FileBackendStore {
 
        /** @var CF_Connection */
        protected $conn; // Swift connection handle
-       protected $connStarted = 0; // integer UNIX timestamp
-       protected $connException; // CloudFiles exception
+       protected $sessionStarted = 0; // integer UNIX timestamp
+
+       /** @var CloudFilesException */
+       protected $connException;
+       protected $connErrorTime = 0; // UNIX timestamp
+
+       /** @var BagOStuff */
+       protected $srvCache;
 
        /** @var ProcessCacheLRU */
        protected $connContainerCache; // container object cache
@@ -75,6 +81,8 @@ class SwiftFileBackend extends FileBackendStore {
         *                             - levels : the number of hash levels (and digits)
         *                             - repeat : hash subdirectories are prefixed with all the
         *                                        parent hash directory names (e.g. "a/ab/abc")
+        *   - cacheAuthInfo      : Whether to cache authentication tokens in APC/XCache.
+        *                          This is probably insecure in shared hosting environments.
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
@@ -103,14 +111,21 @@ class SwiftFileBackend extends FileBackendStore {
                        : false;
                $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
                        ? $config['swiftCDNExpiry']
-                       : 3600; // hour
+                       : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
                $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
                        ? $config['swiftCDNPurgable']
                        : true;
-               // Cache container info to mask latency
+               // Cache container information to mask latency
                $this->memCache = wfGetMainCache();
                // Process cache for container info
                $this->connContainerCache = new ProcessCacheLRU( 300 );
+               // Cache auth token information to avoid RTTs
+               if ( !empty( $config['cacheAuthInfo'] ) ) {
+                       try { // look for APC, XCache, WinCache, ect...
+                               $this->srvCache = ObjectCache::newAccelerator( array() );
+                       } catch ( Exception $e ) {}
+               }
+               $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff();
        }
 
        /**
@@ -118,7 +133,9 @@ class SwiftFileBackend extends FileBackendStore {
         * @return null
         */
        protected function resolveContainerPath( $container, $relStoragePath ) {
-               if ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
+               if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF
+                       return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
+               } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
                        return null; // too long for Swift
                }
                return $relStoragePath;
@@ -712,6 +729,7 @@ class SwiftFileBackend extends FileBackendStore {
                if ( isset( $obj->metadata['Sha1base36'] ) ) {
                        return true; // nothing to do
                }
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
                $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
                if ( $status->isOK() ) {
@@ -722,11 +740,13 @@ class SwiftFileBackend extends FileBackendStore {
                                if ( $hash !== false ) {
                                        $obj->metadata['Sha1base36'] = $hash;
                                        $obj->sync_metadata(); // save to Swift
+                                       wfProfileOut( __METHOD__ );
                                        return true; // success
                                }
                        }
                }
                $obj->metadata['Sha1base36'] = false;
+               wfProfileOut( __METHOD__ );
                return false; // failed
        }
 
@@ -832,13 +852,13 @@ class SwiftFileBackend extends FileBackendStore {
                                                // 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 ) {
+                                               if ( strcmp( $objectDir, $lastDir ) > 0 ) {
                                                        $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
+                                                               && strcmp( $pDir, $lastDir ) > 0 // not done already
                                                                && strlen( $pDir ) > strlen( $dir ) // within $dir
                                                        );
                                                }
@@ -1120,36 +1140,62 @@ class SwiftFileBackend extends FileBackendStore {
        }
 
        /**
-        * Get a connection to the Swift proxy
+        * Get an authenticated connection handle to the Swift proxy
         *
         * @return CF_Connection|bool False on failure
         * @throws CloudFilesException
         */
        protected function getConnection() {
-               if ( $this->connException instanceof Exception ) {
-                       throw $this->connException; // failed last attempt
+               if ( $this->connException instanceof CloudFilesException ) {
+                       if ( ( time() - $this->connErrorTime ) < 60 ) {
+                               throw $this->connException; // failed last attempt; don't bother
+                       } else { // actually retry this time
+                               $this->connException = null;
+                               $this->connErrorTime = 0;
+                       }
                }
                // Session keys expire after a while, so we renew them periodically
-               if ( $this->conn && ( time() - $this->connStarted ) > $this->authTTL ) {
-                       $this->conn->close(); // close active cURL connections
-                       $this->conn = null;
-               }
+               $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL );
                // Authenticate with proxy and get a session key...
-               if ( !$this->conn ) {
-                       $this->connStarted = 0;
+               if ( !$this->conn || $reAuth ) {
+                       $this->sessionStarted = 0;
                        $this->connContainerCache->clear();
-                       try {
-                               $this->auth->authenticate();
-                               $this->conn = new CF_Connection( $this->auth );
-                               $this->connStarted = time();
-                       } catch ( CloudFilesException $e ) {
-                               $this->connException = $e; // don't keep re-trying
-                               throw $e; // throw it back
+                       $cacheKey = $this->getCredsCacheKey( $this->auth->username );
+                       $creds = $this->srvCache->get( $cacheKey ); // credentials
+                       if ( is_array( $creds ) ) { // cache hit
+                               $this->auth->load_cached_credentials(
+                                       $creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] );
+                               $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case
+                       } else { // cache miss
+                               try {
+                                       $this->auth->authenticate();
+                                       $creds = $this->auth->export_credentials();
+                                       $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache
+                                       $this->sessionStarted = time();
+                               } catch ( CloudFilesException $e ) {
+                                       $this->connException = $e; // don't keep re-trying
+                                       $this->connErrorTime = time();
+                                       throw $e; // throw it back
+                               }
+                       }
+                       if ( $this->conn ) { // re-authorizing?
+                               $this->conn->close(); // close active cURL handles in CF_Http object
                        }
+                       $this->conn = new CF_Connection( $this->auth );
                }
                return $this->conn;
        }
 
+       /**
+        * Get the cache key for a container
+        *
+        * @param $username string
+        * @return string
+        */
+       private function getCredsCacheKey( $username ) {
+               return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username );
+       }
+
        /**
         * @see FileBackendStore::doClearCache()
         */