fixed bug 7333: Better solution for "Page is being generated", credits Bernhard Kraft...
authorIngo Renner <ingo.renner@typo3.org>
Thu, 7 Feb 2008 22:40:52 +0000 (22:40 +0000)
committerIngo Renner <ingo.renner@typo3.org>
Thu, 7 Feb 2008 22:40:52 +0000 (22:40 +0000)
git-svn-id: https://svn.typo3.org/TYPO3v4/Core/trunk@3129 709f56b5-9817-0410-a4d7-c38de5d9e867

ChangeLog
t3lib/class.t3lib_lock.php [new file with mode: 0644]
t3lib/config_default.php
typo3/sysext/cms/tslib/class.tslib_fe.php
typo3/sysext/install/mod/class.tx_install.php

index 5d74302..36cfb94 100755 (executable)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,7 +1,11 @@
+2008-02-07  Ingo Renner  <ingo@typo3.org>
+
+       * fixed bug 7333: Better solution for "Page is being generated", credits Bernhard Kraft and Michael Stucki
+
 2008-02-08  Jeff Segars  <jeff@webempoweredchurch.org>
 
        * Fixed bug #7449: Frontend editing fails due to undefined method in t3lib_BEfunc
-       
+
 2008-02-07  Stanislas Rolland  <stanislas.rolland@fructifor.ca>
 
        * Fixed bug #4588: Frontend htmlArea RTE loads global RTE config instead of function parameter $thisConfig
diff --git a/t3lib/class.t3lib_lock.php b/t3lib/class.t3lib_lock.php
new file mode 100644 (file)
index 0000000..02e7f9a
--- /dev/null
@@ -0,0 +1,256 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2008 Michael Stucki (michael@typo3.org)
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*  A copy is found in the textfile GPL.txt and important notices to the license
+*  from the author is found in LICENSE.txt distributed with these scripts.
+*
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+/**
+ * Class for providing locking features in TYPO3
+ *
+ * $Id$
+ *
+ * @author     Michael Stucki <michael@typo3.org>
+ */
+
+require_once(PATH_t3lib.'class.t3lib_div.php');
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * TYPO3 locking class
+ * This class provides an abstract layer to various locking features for TYPO3
+ *
+ * It is intended to blocks requests until some data has been generated.
+ * This is especially useful if two clients are requesting the same website short after each other. While the request of client 1 triggers building and caching of the website, client 2 will be waiting at this lock.
+ *
+ * @author     Michael Stucki <michael@typo3.org>
+ * @package TYPO3
+ * @subpackage t3lib
+ * @see        class.t3lib_tstemplate.php, class.tslib_fe.php
+ */
+class t3lib_lock {
+       private $method;
+       private $id;            // Identifier used for this lock
+       private $filepointer;
+       private $isAcquired = false;
+
+       private $loops = 150;   // Number of times a locked resource is tried to be acquired. This is only used by manual locks like the "simple" method.
+       private $step = 200;    // Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used by manual locks like the "simple" method.
+
+
+
+
+
+       /**
+        * Constructor:
+        * initializes locking, check input parameters and set variables accordingly.
+        *
+        * @param       string          ID to identify this lock in the system
+        * @param       string          Define which locking method to use. Defaults to "simple".
+        * @param       integer         Number of times a locked resource is tried to be acquired. This is only used by manual locks like the "simple" method.
+        * @param       integer         Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used by manual locks like the "simple" method.
+        * @return      boolean         Returns true unless something went wrong
+        */
+       public function __construct($id, $method='', $loops=0, $steps=0)        {
+
+                       // Input checks
+               $id = (string)$id;      // Force ID to be string
+               if (intval($loops)) {
+                       $this->loops = intval($loops);
+               }
+               if (intval($step)) {
+                       $this->step = intval($step);
+               }
+
+                       // Detect locking method
+               if (in_array($method, array('disable', 'simple','flock','semaphore'))) {
+                       $this->method = $method;
+               } else {
+                       throw new Exception('No such method "'.$method.'"');
+               }
+
+               $success = false;
+               switch ($this->method) {
+                       case 'simple':
+                       case 'flock':
+                               $path = PATH_site.'typo3temp/locks/';
+                               $this->id = $path.md5($id);
+                               $success = true;
+                       break;
+                       case 'semaphore':
+                               $id = abs(crc32($id));
+                               if (($this->id = sem_get($id, 1))==true) {
+                                       $success = true;
+                               }
+                       break;
+                       case 'disable':
+                               return false;
+                       break;
+               }
+
+               return $success;
+       }
+
+       /**
+        * Acquire a lock and return when successful. If the lock is already open, the client will be
+        *
+        * It is important to know that the lock will be acquired in any case, even if the request was blocked first. Therefore, the lock needs to be released in every situation.
+        *
+        * @return      boolean         Returns true if lock could be acquired without waiting, false otherwise.
+        */
+       public function acquire()       {
+               $noWait = true; // Default is true, which means continue without caring for other clients. In the case of TYPO3s cache management, this has no negative effect except some resource overhead.
+               $isAcquired = true;
+
+               switch ($this->method) {
+                       case 'simple':
+                               if (is_file($this->id)) {
+                                       $i = 0;
+                                       while ($i<$this->loops) {
+                                               $i++;
+                                               usleep($this->step*1000);
+                                               clearstatcache();
+                                               if (!is_file($this->id)) {      // Lock became free, leave the loop
+                                                       $noWait = false;
+                                                       break;
+                                               }
+                                       }
+                               } else {
+                                       $noWait = true;
+                               }
+
+                               if (($this->filepointer = touch($this->id)) == false) {
+                                       throw new Exception('Lock file could not be created');
+                               }
+                       break;
+                       case 'flock':
+                               if (($this->filepointer = fopen($this->id, 'w+')) == false) {
+                                       throw new Exception('Lock file could not be opened');
+                               }
+
+                               if (flock($this->filepointer, LOCK_EX|LOCK_NB) == true) {       // Lock without blocking
+                                       $noWait = true;
+                               } elseif (flock($this->filepointer, LOCK_EX) == true) {         // Lock with blocking (waiting for similar locks to become released)
+                                       $noWait = false;
+                               } else {
+                                       throw new Exception('Could not lock file "'.$this->id.'"');
+                               }
+                       break;
+                       case 'semaphore':
+                               if (sem_acquire($this->id)) {
+                                               // Unfortunately it seems not possible to find out if the request was blocked, so we return false in any case to make sure the operation is tried again.
+                                       $noWait = false;
+                               }
+                       break;
+                       case 'disable':
+                               $noWait = false;
+                               $isAcquired = false;
+                       break;
+               }
+
+               $this->isAcquired = $isAcquired;
+               return $noWait;
+       }
+
+       /**
+        * Release the lock
+        *
+        * @return      boolean         Returns true on success or false on failure
+        */
+       public function release()       {
+               if (!$this->isAcquired) {
+                       return true;
+               }
+
+               $success = true;
+               switch ($this->method) {
+                       case 'simple':
+                               if (unlink($this->id) == false) {
+                                       $success = false;
+                               }
+                       break;
+                       case 'flock':
+                               if (flock($this->filepointer, LOCK_UN) == false) {
+                                       $success = false;
+                               }
+                               fclose($this->filepointer);
+                               unlink($this->id);
+                       break;
+                       case 'semaphore':
+                               if (!sem_release($this->id)) {
+                                       $success = false;
+                               }
+                       break;
+                       case 'disable':
+                               $success = false;
+                       break;
+               }
+
+               $this->isAcquired = false;
+               return $success;
+       }
+
+       /**
+        * Return the locking method which is currently used
+        *
+        * @return      string          Locking method
+        */
+       public function getMethod()     {
+               return $this->method;
+       }
+
+       /**
+        * Return the ID of which is currently used
+        *
+        * @return      string          Locking ID
+        */
+       public function getId() {
+               return $this->id;
+       }
+
+       /**
+        * Return the status of a lock
+        *
+        * @return      string          Returns true if lock is acquired, false otherwise
+        */
+       public function getLockStatus() {
+               return $this->isAcquired;
+       }
+}
+
+
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_lock.php'])     {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_lock.php']);
+}
+?>
\ No newline at end of file
index e7f5c9c..75e3c53 100755 (executable)
@@ -88,6 +88,7 @@ $TYPO3_CONF_VARS = Array(
                'systemLogLevel' => 0,                                  // Integer: Only messages with same or higher severity are logged; 0 is info, 1 is notice, 2 is warning, 3 is error, 4 is fatal error.
                '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' => 0,                                  // Boolean: If true and [BE][forceCharset] is set to utf-8, 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.
+               '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 existance), "flock" (using PHPs flock() function), "semaphore" (using PHPs sem_acquire() function). Default is "disable".
        ),
        'EXT' => Array (        // Options related to the Extension Management
                'noEdit' => 1,                                                  // Boolean: If set, the Extension Manager does NOT allow extension files to be edited! (Otherwise both local and global extensions can be edited.)
index 91f6196..bc8b876 100755 (executable)
  *
  */
 
+require_once (PATH_t3lib.'class.t3lib_lock.php');
 
 
 
        var $LL_labels_cache=array();
        var $LL_files_cache=array();
 
+       /**
+        * Locking object
+        *
+        * @var t3lib_lock
+        */
+       var $pagesection_lockObj;                               // Locking object for accessing "cache_pagesection"
+
+       /**
+        * Locking object
+        *
+        * @var t3lib_lock
+        */
+       var $pages_lockObj;                                     // Locking object for accessing "cache_pages"
+
 
 
 
        function getFromCache() {
                if (!$this->no_cache) {
                        $cc = $this->tmpl->getCurrentPageData();
+
+                       if (!is_array($cc)) {
+                               $key = $this->id.'::'.$this->MP;
+                               $isLocked = $this->acquirePageGenerationLock($this->pagesection_lockObj, $key); // Returns true if the lock is active now
+                               if (!$isLocked) {       // Lock is no longer active, the data in "cache_pagesection" is now ready
+                                       $cc = $this->tmpl->getCurrentPageData();
+                                       if (is_array($cc)) {
+                                               $this->releasePageGenerationLock($this->pagesection_lockObj);   // Release the lock
+                                       }
+                               }
+                       }
+
                        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->cacheContentFlag = 0;
 
                        // Look for page in cache only if caching is not disabled and if a shift-reload is not sent to the server.
-               if ($this->all && !$this->no_cache && !$this->headerNoCache())  {
+               if (!$this->no_cache && !$this->headerNoCache()) {
+                       $lockHash = $this->getLockHash();
+
+                       if ($this->all) {
+                               $this->newHash = $this->getHash();
 
-                       $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)) {
+                                                               $this->releasePageGenerationLock($this->pages_lockObj); // Release the lock
+                                                       }
+                                               }
+                                       }
 
-                       $GLOBALS['TT']->push('Cache Row','');
-                               $row = $this->getFromCache_queryRow();
-                               if ($row) {
+                                       if (is_array($row)) {
+                                                       // Release this lock
+                                               $this->releasePageGenerationLock($this->pages_lockObj);
 
-                                       $this->config = (array)unserialize($row['cache_data']);         // Fetches the lowlevel config stored with the cached data
-                                       $this->content = $row['HTML'];  // Getting the content
-                                       $this->tempContent = $row['temp_content'];      // Flag for temp content
-                                       $this->cacheContentFlag = 1;    // Setting flag, so we know, that some cached content has been loaded
-                                       $this->cacheExpires = $row['expires'];
+                                               $this->config = (array)unserialize($row['cache_data']);         // Fetches the lowlevel config stored with the cached data
+                                               $this->content = $row['HTML'];  // Getting the content
+                                               $this->tempContent = $row['temp_content'];      // Flag for temp content
+                                               $this->cacheContentFlag = 1;    // Setting flag, so we know, that some cached content has been loaded
+                                               $this->cacheExpires = $row['expires'];
 
-                                       if ($this->TYPO3_CONF_VARS['FE']['debug'] || $this->config['config']['debug'])  {
-                                               $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
-                                               $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
+                                               if ($this->TYPO3_CONF_VARS['FE']['debug'] || $this->config['config']['debug'])  {
+                                                       $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
+                                                       $timeFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
 
-                                               $this->content.= chr(10).'<!-- Cached page generated '.date($dateFormat.' '.$timeFormat, $row['tstamp']).'. Expires '.Date($dateFormat.' '.$timeFormat, $row['expires']).' -->';
+                                                       $this->content.= chr(10).'<!-- Cached page generated '.date($dateFormat.' '.$timeFormat, $row['tstamp']).'. Expires '.Date($dateFormat.' '.$timeFormat, $row['expires']).' -->';
+                                               }
                                        }
-                               }
-                       $GLOBALS['TT']->pull();
+                               $GLOBALS['TT']->pull();
+
+                       } else {
+                               $this->acquirePageGenerationLock($this->pages_lockObj, $lockHash);
+                       }
                }
        }
 
         *
         * @return      string          MD5 hash of $this->hash_base which is a serialized version of there variables.
         * @access private
-        * @see getFromCache()
+        * @see getFromCache(), getLockHash()
         */
        function getHash()      {
                $this->hash_base = serialize(
                return md5($this->hash_base);
        }
 
+       /**
+        * Calculates the lock-hash
+        * This hash is unique to the above hash, except that it doesn't contain the template information in $this->all.
+        *
+        * @return      string          MD5 hash
+        * @access private
+        * @see getFromCache(), getHash()
+        */
+       function getLockHash()  {
+               $lockHash = serialize(
+                       array(
+                               'id' => intval($this->id),
+                               'type' => intval($this->type),
+                               'gr_list' => (string)$this->gr_list,
+                               'MP' => (string)$this->MP,
+                               'cHash' => $this->cHash_array
+                       )
+               );
+
+               return md5($lockHash);
+       }
+
        /**
         * Checks if config-array exists already but if not, gets it
         *
                }
        }
 
+       /**
+        * Lock the page generation process
+        * The lock is used to queue page requests until this page is successfully stored in the cache.
+        *
+        * @param       object          Reference to a locking object
+        * @param       string          String to identify the lock in the system
+        * @return      boolean         Returns true if the lock could be obtained, false otherwise (= process had to wait for existing lock to be released)
+        * @see releasePageGenerationLock()
+        */
+       function acquirePageGenerationLock(&$lockObj, $key)     {
+               if ($this->no_cache || $this->headerNoCache()) {
+                       return true;    // No locking is needed if caching is disabled
+               }
+
+               try {
+                       if (!is_object($lockObj)) {
+                               $className = t3lib_div::makeInstanceClassName('t3lib_lock');
+                               $lockObj = new $className($key, $this->TYPO3_CONF_VARS['SYS']['lockingMode']);
+                       }
+
+                       $success = false;
+                       if (strlen($key)) {
+                                       // true = Page could get locked without blocking
+                                       // false = Page could get locked but process was blocked before
+                               $success = $lockObj->acquire();
+                       }
+               } catch (Exception $e) {
+                       t3lib_div::sysLog('Locking failed: '.$e->getMessage(), 'cms', 3);
+                       $success = false;       // If locking fails, return with false and continue without locking
+               }
+
+               return $success;
+       }
+
+       /**
+        * Release the page generation lock
+        *
+        * @param       object          Reference to a locking object
+        * @return      boolean         Returns true on success, false otherwise
+        * @see acquirePageGenerationLock()
+        */
+       function releasePageGenerationLock(&$lockObj)   {
+               if ($this->no_cache || $this->headerNoCache()) {
+                       return true;    // No locking is needed if caching is disabled
+               }
+
+               $success = false;
+               if (is_object($lockObj)) {
+                       $success = $lockObj->release();
+                       unset($lockObj);
+               }
+
+               return $success;
+       }
+
 
 
 
                $this->newHash = $this->getHash();      // Same codeline as in getFromCache(). But $this->all has been changed by t3lib_TStemplate::start() in the meantime, so this must be called again!
                $this->config['hash_base'] = $this->hash_base;  // For cache management informational purposes.
 
-                       // 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();
+               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.
+                       $this->tempPageCacheContent();
+               }
 
                        // Setting cache_timeout_default. May be overridden by PHP include scritps.
                $this->cacheTimeOutDefault = intval($this->config['config']['cache_period']);
                        $this->tempContent = false;
                }
 
+                       // Release open locks
+               $this->releasePageGenerationLock($this->pagesection_lockObj);
+               $this->releasePageGenerationLock($this->pages_lockObj);
+
                        // Sets sys-last-change:
                $this->setSysLastChanged();
        }
index b38a487..8ade8db 100755 (executable)
@@ -1729,6 +1729,7 @@ From sub-directory:
                        'typo3temp/llxml/' => array('This folder is part of the typo3temp/ section. It needs to be writable, too.',2,'dir_typo3temp'),
                        'typo3temp/cs/' => array('This folder is part of the typo3temp/ section. It needs to be writable, too.',2,'dir_typo3temp'),
                        'typo3temp/GB/' => array('This folder is part of the typo3temp/ section. It needs to be writable, too.',2,'dir_typo3temp'),
+                       'typo3temp/locks/' => array('This folder is part of the typo3temp/ section. It needs to be writable, too.',2,'dir_typo3temp'),
                        'typo3conf/' => array('This directory contains the local configuration files of your website. TYPO3 must be able to write to these configuration files during setup and when the Extension Manager (EM) installs extensions.',2),
                        'typo3conf/ext/' => array('Location for local extensions. Must be writable if the Extension Manager is supposed to install extensions for this website.',0),
                        'typo3conf/l10n/' => array('Location for translations. Must be writable if the Extension Manager is supposed to install translations for extensions.',0),