[FEATURE] Add Locking API 00/37700/20
authorMarkus Klein <klein.t3@reelworx.at>
Sat, 7 Mar 2015 14:09:46 +0000 (15:09 +0100)
committerMarkus Klein <klein.t3@reelworx.at>
Mon, 13 Apr 2015 12:19:06 +0000 (14:19 +0200)
This patch adds a flexible locking API, which allows to be
extended by extensions.

It also deprecates the old API and adjusts all Core usages.

Resolves: #47712
Releases: master
Change-Id: Iff144b4dd7d5d3fed357380f1669a92dece04c2c
Reviewed-on: http://review.typo3.org/37700
Reviewed-by: Helmut Hummel <helmut.hummel@typo3.org>
Tested-by: Helmut Hummel <helmut.hummel@typo3.org>
Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Alexander Opitz <opitz.alexander@googlemail.com>
20 files changed:
typo3/sysext/core/Classes/Core/ClassLoader.php
typo3/sysext/core/Classes/Locking/FileLockStrategy.php [new file with mode: 0644]
typo3/sysext/core/Classes/Locking/LockFactory.php [new file with mode: 0644]
typo3/sysext/core/Classes/Locking/Locker.php
typo3/sysext/core/Classes/Locking/LockingStrategyInterface.php [new file with mode: 0644]
typo3/sysext/core/Classes/Locking/SemaphoreLockStrategy.php [new file with mode: 0644]
typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php [new file with mode: 0644]
typo3/sysext/core/Classes/Mail/MboxTransport.php
typo3/sysext/core/Classes/TypoScript/TemplateService.php
typo3/sysext/core/Classes/Utility/GeneralUtility.php
typo3/sysext/core/Configuration/DefaultConfiguration.php
typo3/sysext/core/Documentation/Changelog/master/Deprecation-47712-DeprecateOldLockingAPI.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-47712-NewLockingAPI.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Locking/FileLockStrategyTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Locking/Fixtures/DummyLock.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Locking/LockFactoryTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Locking/SemaphoreLockStrategyTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Locking/SimpleLockStrategyTest.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
typo3/sysext/frontend/Classes/RequestHandler.php

index 9a6f54a..096ed54 100644 (file)
@@ -14,7 +14,8 @@ namespace TYPO3\CMS\Core\Core;
  * The TYPO3 project - inspiring people to share!
  */
 
-use TYPO3\CMS\Core\Locking\Locker;
+use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Package\PackageInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Cache;
@@ -94,7 +95,7 @@ class ClassLoader {
        protected $isLoadingLocker = FALSE;
 
        /**
-        * @var \TYPO3\CMS\Core\Locking\Locker
+        * @var LockingStrategyInterface
         */
        protected $lockObject = NULL;
 
@@ -724,8 +725,8 @@ class ClassLoader {
                        }
 
                        // We didn't lock yet so do it
-                       if (!$lockObject->getLockStatus()) {
-                               if (!$lockObject->acquireExclusiveLock()) {
+                       if (!$lockObject->isAcquired()) {
+                               if (!$lockObject->acquire()) {
                                        throw new \RuntimeException('Could not acquire lock for ClassLoader cache creation.', 1394480725);
                                }
 
@@ -763,7 +764,7 @@ class ClassLoader {
                // $this->lockObject can be null in installer context without typo3temp, but then this method shouldn't
                // be registered as shutdown-function due to caching being disabled in this case.
                // See @getLocker for more information.
-               if ($error !== NULL && $this->lockObject !== NULL && $this->lockObject->getLockStatus()) {
+               if ($error !== NULL && $this->lockObject !== NULL && $this->lockObject->isAcquired()) {
                        $this->clearClassesCache();
                        $this->releaseLock(TRUE);
                }
@@ -796,14 +797,14 @@ class ClassLoader {
         * Gets the TYPO3 Locker object or creates an instance of it.
         *
         * @throws \RuntimeException
-        * @return \TYPO3\CMS\Core\Locking\Locker|NULL Only NULL if we are in installer and typo3temp does not exist yet
+        * @return LockingStrategyInterface|NULL Only NULL if we are in installer and typo3temp does not exist yet
         */
        protected function getLocker() {
                if (NULL === $this->lockObject) {
                        $this->isLoadingLocker = TRUE;
 
                        try {
-                               $this->lockObject = new Locker('ClassLoader-cache-classes', Locker::LOCKING_METHOD_SIMPLE);
+                               $this->lockObject = (new LockFactory())->createLocker('ClassLoader-cache-classes');
                        } catch (\RuntimeException $e) {
                                // The RuntimeException in constructor happens if directory typo3temp/locks could not be created.
                                // This usually happens during installation step 1 where typo3temp itself does not exist yet. In
@@ -818,7 +819,6 @@ class ClassLoader {
                                        throw $e;
                                }
                        }
-                       $this->lockObject->setEnableLogging(FALSE);
                        $this->isLoadingLocker = FALSE;
                }
 
diff --git a/typo3/sysext/core/Classes/Locking/FileLockStrategy.php b/typo3/sysext/core/Classes/Locking/FileLockStrategy.php
new file mode 100644 (file)
index 0000000..5640773
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+namespace TYPO3\CMS\Core\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * flock() locking
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class FileLockStrategy implements LockingStrategyInterface {
+
+       const FILE_LOCK_FOLDER = 'typo3temp/locks/';
+
+       /**
+        * @var resource File pointer if using flock method
+        */
+       protected $filePointer;
+
+       /**
+        * @var string File used for locking
+        */
+       protected $filePath;
+
+       /**
+        * @var bool True if lock is acquired
+        */
+       protected $isAcquired = FALSE;
+
+       /**
+        * @param string $subject ID to identify this lock in the system
+        * @throws \RuntimeException
+        */
+       public function __construct($subject) {
+               /*
+                * Tests if the directory for simple locks is available.
+                * If not, the directory will be created. The lock path is usually
+                * below typo3temp, typo3temp itself should exist already
+                */
+               $path = PATH_site . self::FILE_LOCK_FOLDER;
+               if (!is_dir($path)) {
+                       // Not using mkdir_deep on purpose here, if typo3temp itself
+                       // does not exist, this issue should be solved on a different
+                       // level of the application.
+                       if (!GeneralUtility::mkdir($path)) {
+                               throw new \RuntimeException('Cannot create directory ' . $path, 1395140007);
+                       }
+               }
+               if (!is_writable($path)) {
+                       throw new \RuntimeException('Cannot write to directory ' . $path, 1396278700);
+               }
+               $this->filePath = $path . md5((string)$subject);
+       }
+
+       /**
+        * Destructor:
+        * Releases lock automatically when instance is destroyed and release resources
+        */
+       public function __destruct() {
+               $this->release();
+       }
+
+       /**
+        * Try to acquire an exclusive lock
+        *
+        * @param int $mode LOCK_CAPABILITY_EXCLUSIVE or LOCK_CAPABILITY_SHARED or self::LOCK_CAPABILITY_NOBLOCK
+        * @return bool Returns TRUE if the lock was acquired successfully
+        * @throws \RuntimeException with code 1428700748 if the acquire would have blocked and NOBLOCK was set
+        */
+       public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE) {
+               if ($this->isAcquired) {
+                       return TRUE;
+               }
+
+               $this->filePointer = fopen($this->filePath, 'c');
+               if ($this->filePointer === FALSE) {
+                       throw new \RuntimeException('Lock file could not be opened', 1294586099);
+               }
+
+               $operation = $mode & self::LOCK_CAPABILITY_EXCLUSIVE ? LOCK_EX : LOCK_SH;
+               if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
+                       $operation |= LOCK_NB;
+               }
+
+               $wouldBlock = 0;
+               $this->isAcquired = flock($this->filePointer, $operation, $wouldBlock);
+
+               if ($mode & self::LOCK_CAPABILITY_NOBLOCK && !$this->isAcquired && $wouldBlock) {
+                       throw new \RuntimeException('Failed to acquire lock because the request would block.', 1428700748);
+               }
+
+               return $this->isAcquired;
+       }
+
+       /**
+        * Release the lock
+        *
+        * @return bool Returns TRUE on success or FALSE on failure
+        */
+       public function release() {
+               if (!$this->isAcquired) {
+                       return TRUE;
+               }
+               $success = TRUE;
+               if (is_resource($this->filePointer)) {
+                       if (flock($this->filePointer, LOCK_UN) === FALSE) {
+                               $success = FALSE;
+                       }
+                       fclose($this->filePointer);
+               }
+               $this->isAcquired = FALSE;
+               return $success;
+       }
+
+       /**
+        * Get status of this lock
+        *
+        * @return bool Returns TRUE if lock is acquired by this locker, FALSE otherwise
+        */
+       public function isAcquired() {
+               return $this->isAcquired;
+       }
+
+       /**
+        * @return int Returns a priority for the method. 0 to 100, 100 is highest
+        */
+       static public function getPriority() {
+               return 50;
+       }
+
+       /**
+        * @return int LOCK_CAPABILITY_* elements combined with bit-wise OR
+        */
+       static public function getCapabilities() {
+               if (PHP_SAPI === 'isapi') {
+                       // From php docs: When using a multithreaded server API like ISAPI you may not be able to rely on flock()
+                       // to protect files against other PHP scripts running in parallel threads of the same server instance!
+                       return 0;
+               }
+               $capabilities = self::LOCK_CAPABILITY_EXCLUSIVE | self::LOCK_CAPABILITY_SHARED | self::LOCK_CAPABILITY_NOBLOCK;
+               return $capabilities;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Locking/LockFactory.php b/typo3/sysext/core/Classes/Locking/LockFactory.php
new file mode 100644 (file)
index 0000000..8f5c62a
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+namespace TYPO3\CMS\Core\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+
+/**
+ * Factory class to retrieve a locking method
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class LockFactory implements SingletonInterface {
+
+       /**
+        * @var bool[]
+        */
+       protected $lockingStrategy = array(
+               SemaphoreLockStrategy::class => TRUE,
+               FileLockStrategy::class => TRUE,
+               SemaphoreLockStrategy::class => TRUE
+       );
+
+       /**
+        * Add a locking method
+        *
+        * @param string $className
+        * @throws \InvalidArgumentException
+        */
+       public function addLockingStrategy($className) {
+               $interfaces = class_implements($className);
+               if (isset($interfaces[LockingStrategyInterface::class])) {
+                       $this->lockingStrategy[$className] = TRUE;
+               } else {
+                       throw new \InvalidArgumentException('The given class name ' . $className . ' does not implement the required LockingStrategyInterface interface.', 1425990198);
+               }
+       }
+
+       /**
+        * Remove a locking method
+        *
+        * @param string $className
+        */
+       public function removeLockingStrategy($className) {
+               unset($this->lockingStrategy[$className]);
+       }
+
+       /**
+        * Get best matching locking method
+        *
+        * @param string $id ID to identify this lock in the system
+        * @param int $capabilities LockingStrategyInterface::LOCK_CAPABILITY_* elements combined with bit-wise OR
+        * @return LockingStrategyInterface Class name for a locking method
+        * @throws \InvalidArgumentException
+        */
+       public function createLocker($id, $capabilities = LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE) {
+               $queue = new \SplPriorityQueue();
+
+               /** @var LockingStrategyInterface $method */
+               foreach ($this->lockingStrategy as $method => $_) {
+                       if ($capabilities & $method::getCapabilities()) {
+                               $queue->insert($method, $method::getPriority());
+                       }
+               }
+               if ($queue->count() > 0) {
+                       $className = $queue->top();
+                       // We use 'new' here on purpose!
+                       // Locking might be used very early in the bootstrap process, where makeInstance() does not work
+                       return new $className($id);
+               }
+               throw new \InvalidArgumentException('Could not find a matching locking method with requested capabilities.', 1425990190);
+       }
+
+}
index 050e85d..d9b2bb2 100644 (file)
@@ -92,8 +92,10 @@ class Locker {
         * @param int $step Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock.
         * @throws \RuntimeException
         * @throws \InvalidArgumentException
+        * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
         */
        public function __construct($id, $method = self::LOCKING_METHOD_SIMPLE, $loops = 0, $step = 0) {
+               GeneralUtility::logDeprecatedFunction();
                // Force ID to be string
                $id = (string)$id;
                if ((int)$loops) {
diff --git a/typo3/sysext/core/Classes/Locking/LockingStrategyInterface.php b/typo3/sysext/core/Classes/Locking/LockingStrategyInterface.php
new file mode 100644 (file)
index 0000000..38733d0
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+namespace TYPO3\CMS\Core\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Interface for locking methods
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+interface LockingStrategyInterface {
+
+       /**
+        * Exclusive locks can be acquired
+        */
+       const LOCK_CAPABILITY_EXCLUSIVE = 1;
+
+       /**
+        * Shared locks can be acquired
+        */
+       const LOCK_CAPABILITY_SHARED = 2;
+
+       /**
+        * Do not block when acquiring the lock
+        */
+       const LOCK_CAPABILITY_NOBLOCK = 4;
+
+       /**
+        * @return int LOCK_CAPABILITY_* elements combined with bit-wise OR
+        */
+       static public function getCapabilities();
+
+       /**
+        * @return int Returns a priority for the method. 0 to 100, 100 is highest
+        */
+       static public function getPriority();
+
+       /**
+        * @param string $subject ID to identify this lock in the system
+        */
+       public function __construct($subject);
+
+       /**
+        * Try to acquire a lock
+        *
+        * @param int $mode LOCK_CAPABILITY_EXCLUSIVE or LOCK_CAPABILITY_SHARED
+        * @return bool Returns TRUE if the lock was acquired successfully
+        * @throws \RuntimeException with code 1428700748 if the acquire would have blocked and NOBLOCK was set
+        */
+       public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE);
+
+       /**
+        * Release the lock
+        *
+        * @return bool Returns TRUE on success or FALSE on failure
+        */
+       public function release();
+
+       /**
+        * Get status of this lock
+        *
+        * @return bool Returns TRUE if lock is acquired by this locker, FALSE otherwise
+        */
+       public function isAcquired();
+
+}
diff --git a/typo3/sysext/core/Classes/Locking/SemaphoreLockStrategy.php b/typo3/sysext/core/Classes/Locking/SemaphoreLockStrategy.php
new file mode 100644 (file)
index 0000000..0b0d02c
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+namespace TYPO3\CMS\Core\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Semaphore locking
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class SemaphoreLockStrategy implements LockingStrategyInterface {
+
+       /**
+        * @var mixed Identifier used for this lock
+        */
+       protected $id;
+
+       /**
+        * @var resource Semaphore Resource used for this lock
+        */
+       protected $resource;
+
+       /**
+        * @var bool TRUE if lock is acquired
+        */
+       protected $isAcquired = FALSE;
+
+       /**
+        * @param string $subject ID to identify this lock in the system
+        */
+       public function __construct($subject) {
+               $this->id = abs(crc32((string)$subject));
+       }
+
+       /**
+        * Destructor
+        */
+       public function __destruct() {
+               $this->release();
+               // We do not call sem_remove() since this would remove the resource for other processes,
+               // we leave that to the system. This is not clean, but there's no other way to determine when
+               // a semaphore is no longer needed.
+       }
+
+       /**
+        * Release the lock
+        *
+        * @return bool Returns TRUE on success or FALSE on failure
+        */
+       public function release() {
+               if (!$this->isAcquired) {
+                       return TRUE;
+               }
+               $this->isAcquired = FALSE;
+               return (bool)@sem_release($this->resource);
+       }
+
+       /**
+        * Get status of this lock
+        *
+        * @return bool Returns TRUE if lock is acquired by this locker, FALSE otherwise
+        */
+       public function isAcquired() {
+               return $this->isAcquired;
+       }
+
+       /**
+        * @return int LOCK_CAPABILITY_* elements combined with bit-wise OR
+        */
+       static public function getCapabilities() {
+               if (function_exists('sem_get')) {
+                       return self::LOCK_CAPABILITY_EXCLUSIVE;
+               }
+               return 0;
+       }
+
+       /**
+        * Try to acquire a lock
+        *
+        * @param int $mode LOCK_CAPABILITY_EXCLUSIVE
+        * @return bool Returns TRUE if the lock was acquired successfully
+        * @throws \RuntimeException
+        */
+       public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE) {
+               if ($this->isAcquired) {
+                       return TRUE;
+               }
+
+               $this->resource = sem_get($this->id, 1);
+               if ($this->resource === FALSE) {
+                       throw new \RuntimeException('Unable to get semaphore with id ' . $this->id, 1313828196);
+               }
+
+               $this->isAcquired = (bool)sem_acquire($this->resource);
+               return $this->isAcquired;
+       }
+
+       /**
+        * @return int Returns a priority for the method. 0 to 100, 100 is highest
+        */
+       static public function getPriority() {
+               return 75;
+       }
+
+}
diff --git a/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php b/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php
new file mode 100644 (file)
index 0000000..5a355d2
--- /dev/null
@@ -0,0 +1,181 @@
+<?php
+namespace TYPO3\CMS\Core\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Simple file locking
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class SimpleLockStrategy implements LockingStrategyInterface {
+
+       const FILE_LOCK_FOLDER = 'typo3temp/locks/';
+
+       /**
+        * @var string File path used for this lock
+        */
+       protected $filePath;
+
+       /**
+        * @var bool True if lock is acquired
+        */
+       protected $isAcquired = FALSE;
+
+       /**
+        * @var int Number of times a locked resource is tried to be acquired. Only used in manual locks method "simple".
+        */
+       protected $loops = 150;
+
+       /**
+        * @var int Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used in manual lock method "simple".
+        */
+       protected $step = 200;
+
+       /**
+        * @param string $subject ID to identify this lock in the system
+        * @throws \RuntimeException
+        */
+       public function __construct($subject) {
+               // Tests if the directory for simple locks is available.
+               // If not, the directory will be created. The lock path is usually
+               // below typo3temp, typo3temp itself should exist already
+               $path = PATH_site . self::FILE_LOCK_FOLDER;
+               if (!is_dir($path)) {
+                       // Not using mkdir_deep on purpose here, if typo3temp itself
+                       // does not exist, this issue should be solved on a different
+                       // level of the application.
+                       if (!GeneralUtility::mkdir($path)) {
+                               throw new \RuntimeException('Cannot create directory ' . $path, 1395140007);
+                       }
+               }
+               if (!is_writable($path)) {
+                       throw new \RuntimeException('Cannot write to directory ' . $path, 1396278700);
+               }
+               $this->filePath = $path . md5((string)$subject);
+       }
+
+       /**
+        * @param int $loops Number of times a locked resource is tried to be acquired.
+        * @param int $step Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock.
+        * @return void
+        */
+       public function init($loops = 0, $step = 0) {
+               $this->loops = (int)$loops;
+               $this->step = (int)$step;
+       }
+
+       /**
+        * Destructor:
+        * Releases lock automatically when instance is destroyed and release resources
+        */
+       public function __destruct() {
+               $this->release();
+       }
+
+       /**
+        * Release the lock
+        *
+        * @return bool Returns TRUE on success or FALSE on failure
+        */
+       public function release() {
+               if (!$this->isAcquired) {
+                       return TRUE;
+               }
+
+               $success = TRUE;
+               if (
+                       GeneralUtility::isAllowedAbsPath($this->filePath)
+                       && GeneralUtility::isFirstPartOfStr($this->filePath, PATH_site . self::FILE_LOCK_FOLDER)
+               ) {
+                       if (@unlink($this->filePath) === FALSE) {
+                               $success = FALSE;
+                       }
+               }
+
+               $this->isAcquired = FALSE;
+               return $success;
+       }
+
+       /**
+        * Get status of this lock
+        *
+        * @return bool Returns TRUE if lock is acquired by this locker, FALSE otherwise
+        */
+       public function isAcquired() {
+               return $this->isAcquired;
+       }
+
+       /**
+        * @return int LOCK_CAPABILITY_* elements combined with bit-wise OR
+        */
+       static public function getCapabilities() {
+               return self::LOCK_CAPABILITY_EXCLUSIVE | self::LOCK_CAPABILITY_NOBLOCK;
+       }
+
+       /**
+        * Try to acquire a lock
+        *
+        * @param int $mode LOCK_CAPABILITY_EXCLUSIVE or self::LOCK_CAPABILITY_NOBLOCK
+        * @return bool Returns TRUE if the lock was acquired successfully
+        * @throws \RuntimeException with code 1428700748 if the acquire would have blocked and NOBLOCK was set
+        */
+       public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE) {
+               if ($this->isAcquired) {
+                       return TRUE;
+               }
+
+               if (file_exists($this->filePath)) {
+                       $maxExecutionTime = (int)ini_get('max_execution_time');
+                       $maxAge = time() - ($maxExecutionTime ?: 120);
+                       if (@filectime($this->filePath) < $maxAge) {
+                               // Remove stale lock file
+                               @unlink($this->filePath);
+                       }
+               }
+
+               $this->isAcquired = FALSE;
+               $wouldBlock = FALSE;
+               for ($i = 0; $i < $this->loops; $i++) {
+                       $filePointer = @fopen($this->filePath, 'x');
+                       if ($filePointer !== FALSE) {
+                               fclose($filePointer);
+                               GeneralUtility::fixPermissions($this->filePath);
+                               $this->isAcquired = TRUE;
+                               break;
+                       }
+                       if ($mode & self::LOCK_CAPABILITY_NOBLOCK) {
+                               $wouldBlock = TRUE;
+                               break;
+                       }
+                       usleep($this->step * 1000);
+               }
+
+               if ($mode & self::LOCK_CAPABILITY_NOBLOCK && !$this->isAcquired && $wouldBlock) {
+                       throw new \RuntimeException('Failed to acquire lock because the request would block.', 1428700748);
+               }
+
+               return $this->isAcquired;
+       }
+
+       /**
+        * @return int Returns a priority for the method. 0 to 100, 100 is highest
+        */
+       static public function getPriority() {
+               return 25;
+       }
+
+}
index c8b963c..897d750 100644 (file)
@@ -14,6 +14,9 @@ namespace TYPO3\CMS\Core\Mail;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Locking\LockFactory;
+
 /**
  * Adapter for Swift_Mailer to be used by TYPO3 extensions.
  *
@@ -76,9 +79,9 @@ class MboxTransport implements \Swift_Transport {
                // Add the complete mail inclusive headers
                $messageStr .= $message->toString();
                $messageStr .= LF . LF;
-               $lockObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Locking\Locker::class, $this->debugFile);
-               /** @var \TYPO3\CMS\Core\Locking\Locker $lockObject */
-               $lockObject->acquireExclusiveLock();
+               $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+               $lockObject = $lockFactory->createLocker($this->debugFile);
+               $lockObject->acquire();
                // Write the mbox file
                $file = @fopen($this->debugFile, 'a');
                if (!$file) {
@@ -87,7 +90,7 @@ class MboxTransport implements \Swift_Transport {
                }
                @fwrite($file, $messageStr);
                @fclose($file);
-               \TYPO3\CMS\Core\Utility\GeneralUtility::fixPermissions($this->debugFile);
+               GeneralUtility::fixPermissions($this->debugFile);
                $lockObject->release();
                // Return every receipient as "delivered"
                $count = count((array)$message->getTo()) + count((array)$message->getCc()) + count((array)$message->getBcc());
index 90e1ee7..1949dfa 100644 (file)
@@ -399,7 +399,7 @@ class TemplateService {
         *
         * NOTE about currentPageData:
         * It holds information about the TypoScript conditions along with the list
-        * of template uid's which is used on the page. In the getFromCache function
+        * of template uid's which is used on the page. In the getFromCache() function
         * in TSFE, currentPageData is used to evaluate if there is a template and
         * if the matching conditions are alright. Unfortunately this does not take
         * into account if the templates in the rowSum of currentPageData has
index 24e60b0..c838eef 100755 (executable)
@@ -4951,10 +4951,6 @@ Connection: close
                }
                $date = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ': ');
                if (in_array('file', $log) !== FALSE) {
-                       // In case lock is acquired before autoloader was defined:
-                       if (class_exists(\TYPO3\CMS\Core\Locking\Locker::class) === FALSE) {
-                               require_once ExtensionManagementUtility::extPath('core') . 'Classes/Locking/Locker.php';
-                       }
                        // Write a longer message to the deprecation log
                        $destination = static::getDeprecationLogFileName();
                        $file = @fopen($destination, 'a');
index 81634f3..37b5fbd 100644 (file)
@@ -106,7 +106,7 @@ return array(
                'maxFileNameLength' => 60,                              // Integer: This is the maximum file name length. The value will be taken into account by basic file operations like renaming or creation of files and folders.
                'UTF8filesystem' => FALSE,                              // Boolean: If TRUE then TYPO3 uses utf-8 to store file names. This allows for accented Latin letters as well as any other non-latin characters like Cyrillic and Chinese.
                'systemLocale' => '',                                   // String: locale used for certain system related functions, e.g. escaping shell commands. If problems with filenames containing special characters occur, the value of this option is probably wrong. See <a href="http://php.net/manual/en/function.setlocale.php" target="_blank">setlocale()</a>.
-               'lockingMode' => 'simple',                              // String: Define which locking mode is used to control requests to pages being generated. Can be one of either "disable" (no locking), "simple" (checks for file existence), "flock" (using PHPs <a href="http://php.net/flock" target="_blank">flock()</a> function), "semaphore" (using PHPs <a href="http://php.net/sem-acquire" target="_blank">sem_acquire()</a> function). Default is "simple".
+               'lockingMode' => 'simple',                              // String: *deprecated* Define which locking mode is used to control requests to pages being generated. Can be one of either "disable" (no locking), "simple" (checks for file existence), "flock" (using PHPs <a href="http://php.net/flock" target="_blank">flock()</a> function), "semaphore" (using PHPs <a href="http://php.net/sem-acquire" target="_blank">sem_acquire()</a> function). Default is "simple". (This option is deprecated since TYPO3 CMS 7 and will be removed in TYPO3 CMS 8. The option is only used by extensions using the old Locker.)
                'reverseProxyIP' => '',                                 // String: list of IP addresses. If TYPO3 is behind one or more (intransparent) reverese proxies the IP addresses must be added here.
                'reverseProxyHeaderMultiValue' => 'none',       // String: "none","first","last": defines which values of a proxy header (eg HTTP_X_FORWARDED_FOR) to use, if more than one is found. "none" discards the value, "first" and "last" use the first/last of the values in the list.
                'reverseProxyPrefix' => '',                             // String: optional prefix to be added to the internal URL (SCRIPT_NAME and REQUEST_URI).
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-47712-DeprecateOldLockingAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-47712-DeprecateOldLockingAPI.rst
new file mode 100644 (file)
index 0000000..1e17c06
--- /dev/null
@@ -0,0 +1,26 @@
+===============================================
+Deprecation: #47712 - Deprecate old Locking API
+===============================================
+
+Description
+===========
+
+The old class ``\TYPO3\CMS\Core\Locking\Locker`` is deprecated.
+
+The configuration option [SYS][lockingMode] is deprecated and only affects the old Locker class, which is
+unused in the Core now.
+
+Moreover two unused methods of \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController are deprecated:
+ * acquirePageGenerationLock()
+ * releasePageGenerationLock()
+
+
+Impact
+======
+
+Using the old class will trigger deprecation log messages.
+
+Migration
+=========
+
+Use the new Locking Service API instead.
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-47712-NewLockingAPI.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-47712-NewLockingAPI.rst
new file mode 100644 (file)
index 0000000..0b91480
--- /dev/null
@@ -0,0 +1,52 @@
+=================================
+Feature: #47712 - New Locking API
+=================================
+
+Description
+===========
+
+The new Locking API follows a new approach. Due to the problem of a very scattered support of locking methods
+in the various operating systems, the new API introduces a locking service, which provides access to the various
+locking methods. Some basic methods are shipped with the Core, but the available methods may be extended by
+extensions.
+
+A locking method has to implement the ``LockingStrategyInterface``. Each method has a set of capabilities, which
+may vary depending on the current system, and a priority.
+
+If a function requires a lock, the locking service is asked for the best fitting mechanism matching the requested
+capabilities.
+e.g. Semaphore locking is only available on Linux systems.
+
+Usage example
+=============
+
+Acquire a simple exclusive lock:
+
+.. code-block:: php
+
+       $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+       $locker = $lockFactory->createLocker('someId');
+       $locker->acquire() || die('ups, lock couldn\'t be acquired. That should never happen.');
+       ...
+       $locker->release();
+
+
+Some methods also support non-blocking locks:
+
+.. code-block:: php
+
+       $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+       $locker = $lockFactory->createLocker(
+               'someId',
+               LockingStrategyInterface::LOCK_CAPABILITY_SHARED | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK
+       );
+       try {
+               $result = $locker->acquire(LockingStrategyInterface::LOCK_CAPABILITY_SHARED | LockingStrategyInterface::LOCK_CAPABILITY_NOBLOCK);
+       catch (\RuntimeException $e) {
+               if ($e->getCode() === 1428700748) {
+                       // some process owns the lock, let's do something else meanwhile
+               }
+       }
+       if ($result) {
+               $locker->release();
+       }
diff --git a/typo3/sysext/core/Tests/Unit/Locking/FileLockStrategyTest.php b/typo3/sysext/core/Tests/Unit/Locking/FileLockStrategyTest.php
new file mode 100644 (file)
index 0000000..5637430
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Locking\FileLockStrategy;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Locking\FileLockStrategy
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class FileLockStrategyTest extends UnitTestCase {
+
+       /**
+        * @test
+        */
+       public function constructorCreatesLockDirectoryIfNotExisting() {
+               GeneralUtility::rmdir(PATH_site . FileLockStrategy::FILE_LOCK_FOLDER, TRUE);
+               new FileLockStrategy('999999999');
+               $this->assertTrue(is_dir(PATH_site . FileLockStrategy::FILE_LOCK_FOLDER));
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Locking/Fixtures/DummyLock.php b/typo3/sysext/core/Tests/Unit/Locking/Fixtures/DummyLock.php
new file mode 100644 (file)
index 0000000..8b1e55b
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Locking\Fixtures;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+
+/**
+ * Dummy locking
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class DummyLock implements LockingStrategyInterface {
+
+       /**
+        * @return int LOCK_CAPABILITY_* elements combined with bit-wise OR
+        */
+       static public function getCapabilities() {
+               return self::LOCK_CAPABILITY_EXCLUSIVE;
+       }
+
+       /**
+        * @return int Returns a priority for the method. 0 to 100, 100 is highest
+        */
+       static public function getPriority() {
+               return 100;
+       }
+
+       /**
+        * @param string $subject ID to identify this lock in the system
+        */
+       public function __construct($subject) {
+       }
+
+       /**
+        * Try to acquire a lock
+        *
+        * @param int $mode LOCK_CAPABILITY_EXCLUSIVE or LOCK_CAPABILITY_SHARED
+        * @return bool Returns TRUE if the lock was acquired successfully
+        */
+       public function acquire($mode = self::LOCK_CAPABILITY_EXCLUSIVE) {
+               return FALSE;
+       }
+
+       /**
+        * Release the lock
+        *
+        * @return bool Returns TRUE on success or FALSE on failure
+        */
+       public function release() {
+               return FALSE;
+       }
+
+       /**
+        * Get status of this lock
+        *
+        * @return bool Returns TRUE if lock is acquired by this locker, FALSE otherwise
+        */
+       public function isAcquired() {
+               return FALSE;
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Locking/LockFactoryTest.php b/typo3/sysext/core/Tests/Unit/Locking/LockFactoryTest.php
new file mode 100644 (file)
index 0000000..96c66c3
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Locking\FileLockStrategy;
+use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+use TYPO3\CMS\Core\Locking\LockFactory;
+use TYPO3\CMS\Core\Locking\SemaphoreLockStrategy;
+use TYPO3\CMS\Core\Tests\Unit\Locking\Fixtures\DummyLock;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Locking\LockFactory
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class LockFactoryTest extends UnitTestCase {
+
+       /**
+        * @var LockFactory|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface
+        */
+       protected $mockFactory;
+
+       /**
+        * Set up the tests
+        */
+       protected function setUp() {
+               $this->mockFactory = $this->getAccessibleMock(LockFactory::class, ['dummy']);
+       }
+
+       /**
+        * @test
+        */
+       public function addLockingStrategyAddsTheClassNameToTheInternalArray() {
+               $this->mockFactory->addLockingStrategy(DummyLock::class);
+               $this->assertArrayHasKey(DummyLock::class, $this->mockFactory->_get('lockingStrategy'));
+       }
+
+       /**
+        * @test
+        * @expectedException \InvalidArgumentException
+        * @expectedExceptionCode 1425990198
+        */
+       public function addLockingStrategyThrowsExceptionIfInterfaceIsNotImplemented() {
+               $this->mockFactory->addLockingStrategy(\stdClass::class);
+       }
+
+       /**
+        * @test
+        */
+       public function getLockerReturnsExpectedClass() {
+               $this->mockFactory->_set('lockingStrategy', [FileLockStrategy::class => TRUE]);
+               $locker = $this->mockFactory->createLocker('id', LockingStrategyInterface::LOCK_CAPABILITY_EXCLUSIVE | LockingStrategyInterface::LOCK_CAPABILITY_SHARED);
+               $this->assertInstanceOf(FileLockStrategy::class, $locker);
+       }
+
+       /**
+        * @test
+        */
+       public function getLockerReturnsClassWithHighestPriority() {
+               $this->mockFactory->_set('lockingStrategy', [SemaphoreLockStrategy::class => TRUE, DummyLock::class => TRUE]);
+               $locker = $this->mockFactory->createLocker('id');
+               $this->assertInstanceOf(DummyLock::class, $locker);
+       }
+
+       /**
+        * @test
+        * @expectedException \InvalidArgumentException
+        * @expectedExceptionCode 1425990190
+        */
+       public function getLockerThrowsExceptionIfNoMatchFound() {
+               $this->mockFactory->createLocker('id', 32);
+       }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Locking/SemaphoreLockStrategyTest.php b/typo3/sysext/core/Tests/Unit/Locking/SemaphoreLockStrategyTest.php
new file mode 100644 (file)
index 0000000..6e23b57
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Locking\SemaphoreLockStrategy;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Locking\SemaphoreLockStrategy
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class SemaphoreLockStrategyTest extends UnitTestCase {
+
+       /**
+        * Set up the tests
+        */
+       protected function setUp() {
+               if (!SemaphoreLockStrategy::getCapabilities()) {
+                       $this->markTestSkipped('The system does not support semaphore locking.');
+               }
+       }
+
+       /**
+        * @test
+        */
+       public function acquireGetsSemaphore() {
+               $lock = new SemaphoreLockStrategy('99999');
+               $this->assertTrue($lock->acquire());
+               $lock->release();
+       }
+
+}
diff --git a/typo3/sysext/core/Tests/Unit/Locking/SimpleLockStrategyTest.php b/typo3/sysext/core/Tests/Unit/Locking/SimpleLockStrategyTest.php
new file mode 100644 (file)
index 0000000..25a38c9
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+namespace TYPO3\CMS\Core\Tests\Unit\Locking;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Locking\SimpleLockStrategy;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Testcase for \TYPO3\CMS\Core\Locking\SimpleLockStrategy
+ *
+ * @author Markus Klein <klein.t3@reelworx.at>
+ */
+class SimpleLockStrategyTest extends UnitTestCase {
+
+       /**
+        * @test
+        */
+       public function constructorCreatesLockDirectoryIfNotExisting() {
+               GeneralUtility::rmdir(PATH_site . SimpleLockStrategy::FILE_LOCK_FOLDER, TRUE);
+               new SimpleLockStrategy('999999999');
+               $this->assertTrue(is_dir(PATH_site . SimpleLockStrategy::FILE_LOCK_FOLDER));
+       }
+
+       /**
+        * @test
+        */
+       public function constructorSetsResourceToPathWithIdIfUsingSimpleLocking() {
+               $lock = $this->getAccessibleMock(SimpleLockStrategy::class, ['dummy'], ['999999999']);
+               $this->assertSame(PATH_site . SimpleLockStrategy::FILE_LOCK_FOLDER . md5('999999999'), $lock->_get('filePath'));
+       }
+
+       /**
+        * @test
+        */
+       public function acquireFixesPermissionsOnLockFile() {
+               if (TYPO3_OS === 'WIN') {
+                       $this->markTestSkipped('Test not available on Windows.');
+               }
+               // Use a very high id to be unique
+               /** @var SimpleLockStrategy|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $lock */
+               $lock = $this->getAccessibleMock(SimpleLockStrategy::class, ['dummy'], ['999999999']);
+
+               $pathOfLockFile = $lock->_get('filePath');
+
+               $GLOBALS['TYPO3_CONF_VARS']['BE']['fileCreateMask'] = '0777';
+
+               // Acquire lock, get actual file permissions and clean up
+               $lock->acquire();
+               clearstatcache();
+               $resultFilePermissions = substr(decoct(fileperms($pathOfLockFile)), 2);
+               $lock->release();
+               $this->assertEquals($resultFilePermissions, '0777');
+       }
+
+       /**
+        * @test
+        */
+       public function releaseRemovesLockfileInTypo3TempLocks() {
+               /** @var SimpleLockStrategy|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $lock */
+               $lock = $this->getAccessibleMock(SimpleLockStrategy::class, ['dummy'], ['999999999']);
+
+               $pathOfLockFile = $lock->_get('filePath');
+
+               $lock->acquire();
+               $lock->release();
+
+               $this->assertFalse(is_file($pathOfLockFile));
+       }
+
+       /**
+        * Dataprovider for releaseDoesNotRemoveFilesNotWithinTypo3TempLocksDirectory
+        */
+       public function invalidFileReferences() {
+               return array(
+                       'not withing PATH_site' => array('/tmp/TYPO3-Lock-Test'),
+                       'directory traversal' => array(PATH_site . 'typo3temp/../typo3temp/locks/foo'),
+                       'directory traversal 2' => array(PATH_site . 'typo3temp/locks/../locks/foo'),
+                       'within uploads' => array(PATH_site . 'uploads/TYPO3-Lock-Test')
+               );
+       }
+
+       /**
+        * @test
+        * @dataProvider invalidFileReferences
+        * @param string $file
+        * @throws \PHPUnit_Framework_SkippedTestError
+        */
+       public function releaseDoesNotRemoveFilesNotWithinTypo3TempLocksDirectory($file) {
+               if (TYPO3_OS === 'WIN') {
+                       $this->markTestSkipped('releaseDoesNotRemoveFilesNotWithinTypo3TempLocksDirectory() test not available on Windows.');
+               }
+               // Create test file
+               touch($file);
+               if (!is_file($file)) {
+                       $this->markTestIncomplete('releaseDoesNotRemoveFilesNotWithinTypo3TempLocksDirectory() skipped: Test file could not be created');
+               }
+               // Create instance, set lock file to invalid path
+               /** @var SimpleLockStrategy|\PHPUnit_Framework_MockObject_MockObject|\TYPO3\CMS\Core\Tests\AccessibleObjectInterface $lock */
+               $lock = $this->getAccessibleMock(SimpleLockStrategy::class, ['dummy'], ['999999999']);
+               $lock->_set('filePath', $file);
+               $lock->_set('isAcquired', TRUE);
+
+               // Call release method
+               $lock->release();
+               // Check if file is still there and clean up
+               $fileExists = is_file($file);
+               if (is_file($file)) {
+                       unlink($file);
+               }
+               $this->assertTrue($fileExists);
+       }
+
+}
index cfd97bb..1e0e19b 100644 (file)
@@ -28,6 +28,8 @@ use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\HttpUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
@@ -786,14 +788,14 @@ class TypoScriptFrontendController {
        /**
         * Locking object for accessing "cache_pagesection"
         *
-        * @var Locker
+        * @var LockingStrategyInterface
         */
        public $pagesection_lockObj;
 
        /**
         * Locking object for accessing "cache_pages"
         *
-        * @var Locker
+        * @var LockingStrategyInterface
         */
        public $pages_lockObj;
 
@@ -2244,60 +2246,82 @@ class TypoScriptFrontendController {
         * See if page is in cache and get it if so
         * Stores the page content in $this->content if something is found.
         *
-        * @return void
+        * @throws \InvalidArgumentException
+        * @throws \RuntimeException
         */
        public function getFromCache() {
-               if (!$this->no_cache) {
-                       $cc = $this->tmpl->getCurrentPageData();
-                       if (!is_array($cc)) {
-                               $key = $this->id . '::' . $this->MP;
-                               // Returns TRUE if the lock is active now
-                               $isLocked = $this->acquirePageGenerationLock($this->pagesection_lockObj, $key);
-                               if (!$isLocked) {
-                                       // Lock is no longer active, the data in "cache_pagesection" is now ready
-                                       $cc = $this->tmpl->getCurrentPageData();
-                                       if (is_array($cc)) {
-                                               // Release the lock
-                                               $this->releasePageGenerationLock($this->pagesection_lockObj);
-                                       }
-                               }
-                       }
-                       if (is_array($cc)) {
-                               // BE CAREFUL to change the content of the cc-array. This array is serialized and an md5-hash based on this is used for caching the page.
-                               // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
-                               // This array is an identification of the template. If $this->all is empty it's because the template-data is not cached, which it must be.
-                               $cc = $this->tmpl->matching($cc);
-                               ksort($cc);
-                               $this->all = $cc;
-                       }
-                       unset($cc);
-               }
                // clearing the content-variable, which will hold the pagecontent
                $this->content = '';
                // Unsetting the lowlevel config
                unset($this->config);
                $this->cacheContentFlag = FALSE;
-               // Look for page in cache only if caching is not disabled and if a shift-reload is not sent to the server.
-               if (!$this->no_cache && !$this->headerNoCache()) {
-                       $lockHash = $this->getLockHash();
+
+               if ($this->no_cache) {
+                       return;
+               }
+
+               $this->pagesection_lockObj = NULL;
+               $pageSectionCacheContent = $this->tmpl->getCurrentPageData();
+               if (!is_array($pageSectionCacheContent)) {
+                       // nothing in the cache, we acquire an exclusive lock now
+                       $key = $this->id . '::' . $this->MP;
+                       $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+                       $this->pagesection_lockObj = $lockFactory->createLocker($key);
+                       if (!$this->pagesection_lockObj->acquire()) {
+                               throw new \RuntimeException('Could not acquire lock for page section generation.', 1294586098);
+                       }
+                       // query the cache again to see if the data are there meanwhile
+                       $pageSectionCacheContent = $this->tmpl->getCurrentPageData();
+                       if (is_array($pageSectionCacheContent)) {
+                               // we have the content, nice that some other process did the work for us
+                               $this->pagesection_lockObj->release();
+                               $this->pagesection_lockObj = NULL;
+                       } else {
+                               // we keep the lock set, because we are the ones generating the page now and filling the cache
+                               // the lock will be released in releaseLocks()
+                       }
+               }
+
+               if (is_array($pageSectionCacheContent)) {
+                       // BE CAREFUL to change the content of the cc-array. This array is serialized and an md5-hash based on this is used for caching the page.
+                       // If this hash is not the same in here in this section and after page-generation, then the page will not be properly cached!
+                       // This array is an identification of the template. If $this->all is empty it's because the template-data is not cached, which it must be.
+                       $pageSectionCacheContent = $this->tmpl->matching($pageSectionCacheContent);
+                       ksort($pageSectionCacheContent);
+                       $this->all = $pageSectionCacheContent;
+               }
+               unset($pageSectionCacheContent);
+
+               // Look for page in cache only if a shift-reload is not sent to the server.
+               $this->pages_lockObj = NULL;
+               $lockHash = $this->getLockHash();
+               if (!$this->headerNoCache()) {
                        if ($this->all) {
+                               // we got page section information
                                $this->newHash = $this->getHash();
                                $GLOBALS['TT']->push('Cache Row', '');
                                $row = $this->getFromCache_queryRow();
                                if (!is_array($row)) {
-                                       $isLocked = $this->acquirePageGenerationLock($this->pages_lockObj, $lockHash);
-                                       if (!$isLocked) {
-                                               // Lock is no longer active, the data in "cache_pages" is now ready
-                                               $row = $this->getFromCache_queryRow();
-                                               if (is_array($row)) {
-                                                       // Release the lock
-                                                       $this->releasePageGenerationLock($this->pages_lockObj);
-                                               }
+                                       // nothing in the cache, we acquire an exclusive lock now
+                                       $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+                                       $this->pages_lockObj = $lockFactory->createLocker($lockHash);
+                                       if (!$this->pages_lockObj->acquire()) {
+                                               throw new \RuntimeException('Could not acquire lock for page content generation.', 1294586099);
+                                       }
+                                       // query the cache again to see if the data are there meanwhile
+                                       $row = $this->getFromCache_queryRow();
+                                       if (is_array($row)) {
+                                               // we have the content, nice that some other process did the work for us
+                                               $this->pages_lockObj->release();
+                                               $this->pages_lockObj = NULL;
+                                       } else {
+                                               // we keep the lock set, because we are the ones generating the page now and filling the cache
+                                               // the lock will be released in releaseLocks()
                                        }
                                }
                                if (is_array($row)) {
-                                       // Release this lock
-                                       $this->releasePageGenerationLock($this->pages_lockObj);
+                                       // we have data from cache
+
                                        // Call hook when a page is retrieved from cache:
                                        if (is_array($this->TYPO3_CONF_VARS['SC_OPTIONS']['tslib/class.tslib_fe.php']['pageLoadedFromCache'])) {
                                                $_params = array('pObj' => &$this, 'cache_pages_row' => &$row);
@@ -2327,10 +2351,17 @@ class TypoScriptFrontendController {
                                        }
                                }
                                $GLOBALS['TT']->pull();
-                       } else {
-                               $this->acquirePageGenerationLock($this->pages_lockObj, $lockHash);
+
+                               return;
                        }
                }
+               // the user forced rebuilding the page cache or there was no pagesection information
+               // get a lock for the page content so other processes will not interrupt the regeneration
+               $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
+               $this->pages_lockObj = $lockFactory->createLocker($lockHash);
+               if (!$this->pages_lockObj->acquire()) {
+                       throw new \RuntimeException('Could not acquire lock for page content generation.', 1294586100);
+               }
        }
 
        /**
@@ -3051,6 +3082,8 @@ class TypoScriptFrontendController {
                                // In any case we should not begin another rendering process also, so we silently disable caching and render the page ourselves and that's it.
                                // Actually $cachedRow contains content that we could show instead of rendering. Maybe we should do that to gain more performance but then we should set all the stuff done in $this->getFromCache()... For now we stick to this...
                                $this->set_no_cache('Another process wrote into the cache since the beginning of the render process', TRUE);
+
+                               // Since the new Locking API this should never be the case
                        } else {
                                $this->tempContent = TRUE;
                                // This flag shows that temporary content is put in the cache
@@ -3168,8 +3201,10 @@ class TypoScriptFrontendController {
         * @param string $key String to identify the lock in the system
         * @return bool Returns TRUE if the lock could be obtained, FALSE otherwise (= process had to wait for existing lock to be released)
         * @see releasePageGenerationLock()
+        * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
         */
        public function acquirePageGenerationLock(&$lockObj, $key) {
+               GeneralUtility::logDeprecatedFunction();
                if ($this->no_cache || $this->headerNoCache()) {
                        GeneralUtility::sysLog('Locking: Page is not cached, no locking required', 'cms', GeneralUtility::SYSLOG_SEVERITY_INFO);
                        // No locking is needed if caching is disabled
@@ -3202,8 +3237,10 @@ class TypoScriptFrontendController {
         * @param Locker $lockObj Reference to a locking object
         * @return bool Returns TRUE on success, FALSE otherwise
         * @see acquirePageGenerationLock()
+        * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
         */
        public function releasePageGenerationLock(&$lockObj) {
+               GeneralUtility::logDeprecatedFunction();
                $success = FALSE;
                // If lock object is set and was acquired (may also happen if no_cache was enabled during runtime), release it:
                if (is_object($lockObj) && $lockObj instanceof Locker && $lockObj->getLockStatus()) {
@@ -3217,6 +3254,23 @@ class TypoScriptFrontendController {
        }
 
        /**
+        * Release pending locks
+        *
+        * @internal
+        * @return void
+        */
+       public function releaseLocks() {
+               if ($this->pagesection_lockObj) {
+                       $this->pagesection_lockObj->release();
+                       $this->pagesection_lockObj = NULL;
+               }
+               if ($this->pages_lockObj) {
+                       $this->pages_lockObj->release();
+                       $this->pages_lockObj = NULL;
+               }
+       }
+
+       /**
         * Adds tags to this page's cache entry, you can then f.e. remove cache
         * entries by tag
         *
@@ -3241,10 +3295,21 @@ class TypoScriptFrontendController {
                // Same codeline as in getFromCache(). But $this->all has been changed by
                // \TYPO3\CMS\Core\TypoScript\TemplateService::start() in the meantime, so this must be called again!
                $this->newHash = $this->getHash();
-               if (!is_object($this->pages_lockObj) || $this->pages_lockObj->getLockStatus() == FALSE) {
-                       // Here we put some temporary stuff in the cache in order to let the first hit generate the page. The temporary cache will expire after a few seconds (typ. 30) or will be cleared by the rendered page, which will also clear and rewrite the cache.
+
+               // If the pages_lock is set, we are in charge of generating the page.
+               if (is_object($this->pages_lockObj)) {
+                       // Here we put some temporary stuff in the cache in order to let the first hit generate the page.
+                       // The temporary cache will expire after a few seconds (typ. 30) or will be cleared by the rendered page,
+                       // which will also clear and rewrite the cache.
                        $this->tempPageCacheContent();
                }
+               // At this point we have a valid pagesection_cache and also some temporary page_cache content,
+               // so let all other processes proceed now. (They are blocked at the pagessection_lock in getFromCaceh())
+               if ($this->pagesection_lockObj) {
+                       $this->pagesection_lockObj->release();
+                       $this->pagesection_lockObj = NULL;
+               }
+
                // Setting cache_timeout_default. May be overridden by PHP include scripts.
                $this->cacheTimeOutDefault = (int)$this->config['config']['cache_period'];
                // Page is generated
@@ -3308,9 +3373,6 @@ class TypoScriptFrontendController {
                        $this->clearPageCacheContent();
                        $this->tempContent = FALSE;
                }
-               // Release open locks
-               $this->releasePageGenerationLock($this->pagesection_lockObj);
-               $this->releasePageGenerationLock($this->pages_lockObj);
                // Sets sys-last-change:
                $this->setSysLastChanged();
        }
index edebcf2..01a6445 100644 (file)
@@ -222,6 +222,7 @@ class RequestHandler implements RequestHandlerInterface {
                                include_once './' . $temp_file;
                        }
                }
+               $this->controller->releaseLocks();
                $this->timeTracker->pull();
 
                // Render non-cached parts