[BUGFIX] Implement locking in ClassLoader 80/28480/2
authorAlexander Opitz <opitz.alexander@googlemail.com>
Fri, 7 Mar 2014 11:37:47 +0000 (12:37 +0100)
committerErnesto Baschny <ernst@cron-it.de>
Mon, 17 Mar 2014 21:16:58 +0000 (22:16 +0100)
After cache clearing we can run in the situation that 2 processes will
rebuild the ClassLoader cache which will lead to a non useable cache,
which produces fatals. As the data is generated more than once the load
of the server increases per request while ClassLoader cache is build.

The implemented Locking will stop this issue. Every process waits till
the first is ready and then looks if data was generated and stops re-
generating the cache.

To-Do: Make it work also for first time installation when there is
no typo3temp/ directory yet.

Resolves: #55099
Releases: 6.2
Change-Id: I9c1916b5b5860e86fe19a1fc292d8ab5a196d960
Reviewed-on: https://review.typo3.org/28480
Reviewed-by: Ernesto Baschny
Tested-by: Ernesto Baschny
typo3/sysext/core/Classes/Core/ClassLoader.php

index 3401ade..8791529 100644 (file)
@@ -27,6 +27,7 @@ namespace TYPO3\CMS\Core\Core;
  *  This copyright notice MUST APPEAR in all copies of the script!
  ***************************************************************/
 
+use TYPO3\CMS\Core\Locking\Locker;
 use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Cache;
@@ -88,6 +89,16 @@ class ClassLoader {
        protected $packageClassesPaths = array();
 
        /**
+        * @var bool Is TRUE while loading the Locker class to prevent a deadlock in the implicit call to loadClass
+        */
+       protected $isLoadingLocker = FALSE;
+
+       /**
+        * @var \TYPO3\CMS\Core\Locking\Locker
+        */
+       protected $lockObject = NULL;
+
+       /**
         * Constructor
         *
         * @param ApplicationContext $context
@@ -139,6 +150,8 @@ class ClassLoader {
         * Loads php files containing classes or interfaces found in the classes directory of
         * a package and specifically registered classes.
         *
+        * Caution: This function may be called "recursively" by the spl_autoloader if a class depends on another classes.
+        *
         * @param string $className Name of the class/interface to load
         * @return bool
         */
@@ -202,13 +215,22 @@ class ClassLoader {
        /**
         * Builds the class loading information and writes it to the cache. It handles Locking for this cache.
         *
+        * Caution: The function loadClass can be called "recursively" by spl_autoloader. This needs to be observed when
+        * locking for cache access. Only the first call to loadClass may acquire and release the lock!
+        *
         * @param string $cacheEntryIdentifier Cache identifier for this class
         * @param string $className Name of class this information is for
         *
         * @return array|null The class information or NULL if class was not found
         */
        protected function buildCachedClassLoadingInformation($cacheEntryIdentifier, $className) {
-               // Look again into cache after we got the look
+               // We do not need locking if we are in earlyCache mode
+               $didLock = FALSE;
+               if (!$this->isEarlyCache) {
+                       $didLock = $this->acquireLock();
+               }
+
+               // Look again into the cache after we got the lock, data might have been generated meanwhile
                $classLoadingInformation = $this->getClassLoadingInformationFromCache($cacheEntryIdentifier);
                if ($classLoadingInformation === NULL) {
                        $classLoadingInformation = $this->buildClassLoadingInformation($className);
@@ -223,12 +245,18 @@ class ClassLoader {
                                $this->classesCache->set($cacheEntryIdentifier, '');
                        }
                }
+
+               $this->releaseLock($didLock);
+
                return $classLoadingInformation;
        }
 
        /**
-        * @param string $className
-        * @return array|null
+        * Builds the class loading information
+        *
+        * @param string $className Name of class this information is for
+        *
+        * @return array|null The class information or NULL if class was not found
         */
        public function buildClassLoadingInformation($className) {
                $classLoadingInformation = $this->buildClassLoadingInformationForClassFromCorePackage($className);
@@ -445,17 +473,24 @@ class ClassLoader {
         * @return void
         */
        protected function buildPackageNamespacesAndClassesPaths() {
-               foreach ($this->packages as $package) {
-                       $this->buildPackageNamespaceAndClassesPath($package);
+               $didLock = $this->acquireLock();
+
+               // Take a look again, after lock is acquired
+               if (!$this->loadPackageNamespacesFromCache()) {
+                       foreach ($this->packages as $package) {
+                               $this->buildPackageNamespaceAndClassesPath($package);
+                       }
+                       $this->sortPackageNamespaces();
+                       $this->savePackageNamespacesAndClassesPathsToCache();
+                       // The class alias map has to be rebuilt first, because ext_autoload files can contain
+                       // old class names that need established class aliases.
+                       $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
+                       $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache($this->packages);
+                       $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
+                       $this->transferRuntimeClassInformationCacheEntriesToClassesCache();
                }
-               $this->sortPackageNamespaces();
-               $this->savePackageNamespacesAndClassesPathsToCache();
-               // The class alias map has to be rebuilt first, because ext_autoload files can contain
-               // old class names that need established class aliases.
-               $classNameToAliasMapping = $this->classAliasMap->setPackages($this->packages)->buildMappingAndInitializeEarlyInstanceMapping();
-               $this->loadClassFilesFromAutoloadRegistryIntoRuntimeClassInformationCache($this->packages);
-               $this->classAliasMap->buildMappingFiles($classNameToAliasMapping);
-               $this->transferRuntimeClassInformationCacheEntriesToClassesCache();
+
+               $this->releaseLock($didLock);
        }
 
        /**
@@ -499,6 +534,7 @@ class ClassLoader {
         * Extracts the namespace from a package
         *
         * @param \TYPO3\Flow\Package\PackageInterface $package
+        * @return void
         */
        protected function buildPackageNamespace(\TYPO3\Flow\Package\PackageInterface $package) {
                $packageNamespace = $package->getNamespace();
@@ -652,4 +688,54 @@ class ClassLoader {
                return static::$staticAliasMap->getAliasesForClassName($className);
        }
 
+       /**
+        * Acquires a lock for the cache if we didn't already lock before.
+        *
+        * @return bool TRUE if the cache was acquired by this call and needs to be released
+        * @throws \RuntimeException
+        */
+       protected function acquireLock() {
+               if (!$this->isLoadingLocker) {
+                       $lockObject = $this->getLocker();
+
+                       // We didn't lock yet so do it
+                       if (!$lockObject->getLockStatus()) {
+                               if (!$lockObject->acquireExclusiveLock()) {
+                                       throw new \RuntimeException('Could not acquire lock for ClassLoader cache creation.', 1394480725);
+                               }
+                               return TRUE;
+                       }
+               }
+               return FALSE;
+       }
+
+       /**
+        * Releases a lock
+        *
+        * @param bool $needRelease The result of the call to acquireLock()
+        *
+        * @return void
+        */
+       protected function releaseLock($needRelease) {
+               if ($needRelease) {
+                       $lockObject = $this->getLocker();
+                       $lockObject->release();
+               }
+       }
+
+       /**
+        * Gets the TYPO3 Locker object or creates an instance of it.
+        *
+        * @return \TYPO3\CMS\Core\Locking\Locker
+        */
+       protected function getLocker() {
+               if (NULL === $this->lockObject) {
+                       $this->isLoadingLocker = TRUE;
+                       $this->lockObject = new Locker('ClassLoader-cache-classes', Locker::LOCKING_METHOD_SIMPLE);
+                       $this->lockObject->setEnableLogging(FALSE);
+                       $this->isLoadingLocker = FALSE;
+               }
+
+               return $this->lockObject;
+       }
 }