Project

General

Profile

Task #55541 » pageCacheRedundancy.diff

Diff with changes - Bernhard Kraft, 2014-02-01 02:57

View differences:

typo3/sysext/core/Classes/Cache/Backend/PageCacheBackend.php
<?php
namespace TYPO3\CMS\Core\Cache\Backend;
/***************************************************************
* Copyright notice
*
* (c) 2014 Bernhard Kraft <kraft@webconsulting.at>
* 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.
*
* 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!
***************************************************************/
/**
* A caching backend especially optimized for being used as the
* TYPO3 page cache
*
* @author Bernhard Kraft <kraft@webconsulting.at>
* @api
*/
class PageCacheBackend extends Typo3DatabaseBackend implements \TYPO3\CMS\Core\Cache\Backend\RedundancyAwareInterface {
/**
* Removes all cache entries matching the specified identifier.
* Usually this affects one page cache entry and up to three field cache entries.
* Redundancy cache entries will only get deleted if they are not used by another page cache entry.
*
* @param string $entryIdentifier Specifies the cache entry to remove
* @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
*/
public function remove($entryIdentifier) {
$removed = parent::remove($entryIdentifier);
if ($removed) {
$this->collectRedundancyGarbage();
}
return $removed;
}
/**
* Removes all cache entries of this cache which are tagged by the specified tag.
*
* @param string $tag The tag the entries must have
* @return void
*/
public function flushByTag($tag) {
parent::flushByTag($tag);
$this->collectRedundancyGarbage();
}
/**
* Does garbage collection
*
* @return void
*/
public function collectGarbage() {
parent::collectGarbage();
$this->collectRedundancyGarbage();
}
/**
* Does redundancy cache garbage collection
*
* @return void
*/
public function collectRedundancyGarbage() {
// This query finds redundancy entries which are not referenced any more by any page cache entries and deletes them
$rowsResource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
'c1.identifier AS identifier',
$this->cacheTable . ' AS c1 LEFT JOIN ' . $this->tagsTable . ' AS c2 ON c1.identifier=c2.tag OR c1.identifier=c2.identifier',
'c2.tag IS NULL'
);
$cacheEntryIdentifiers = array();
while ($cacheRow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($rowsResource)) {
$cacheEntryIdentifiers[] = $GLOBALS['TYPO3_DB']->fullQuoteStr($cacheRow['identifier'], $this->cacheTable);
}
$GLOBALS['TYPO3_DB']->sql_free_result($rowsResource);
if (count($cacheEntryIdentifiers)) {
$GLOBALS['TYPO3_DB']->exec_DELETEquery($this->cacheTable, 'identifier IN (' . implode(', ', $cacheEntryIdentifiers) . ')');
}
}
}
typo3/sysext/core/Classes/Cache/Backend/RedundancyAwareInterface.php
<?php
namespace TYPO3\CMS\Core\Cache\Backend;
/***************************************************************
* Copyright notice
*
* (c) 2014 Bernhard Kraft <kraft@webconsulting.at>
* 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.
*
* 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!
***************************************************************/
/**
* A contract for a cache backend which is able to handle entries
* which are shared among other entries with the purpose to reduce
* the amount of redundant data stored in the cache.
*
* @api
*/
interface RedundancyAwareInterface extends \TYPO3\CMS\Core\Cache\Backend\BackendInterface {
/**
* Does redundancy cache garbage collection
* This is the only method a redundancy aware interface MUST implement.
* The method should be called by the backend at appropriate events i.e. after removing a cache entry.
*
* @return void
*/
public function collectRedundancyGarbage();
}
typo3/sysext/core/Classes/Cache/Frontend/PageCacheEntry.php
<?php
namespace TYPO3\CMS\Core\Cache\Frontend;
/***************************************************************
* Copyright notice
*
* (c) 2014 Bernhard Kraft <kraft@webconsulting.at>
* 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.
*
* 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!
***************************************************************/
/**
* This class represents a "squeezed" entry in the page cache.
*
* If a the contents of page-cache array field ('content' or 'cache_data')
* get put in the cache on their own instead of their orginial value an
* instance of this class will be put into the page-cache array.
*
* This has mostly the sense of recognizing that a field was redundancy
* squeezed so it can get restored on retrieval.
*
*
* @author Bernhard Kraft <kraft@webconsulting.at>
*/
class PageCacheEntry {
protected $identifier = '';
public function __construct($identifier) {
$this->identifier = $identifier;
}
public function getIdentifier() {
return $this->identifier;
}
}
typo3/sysext/core/Classes/Cache/Frontend/PageCacheFrontend.php
<?php
namespace TYPO3\CMS\Core\Cache\Frontend;
/***************************************************************
* Copyright notice
*
* (c) 2014 Bernhard Kraft <kraft@webconsulting.at>
* 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.
*
* 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!
***************************************************************/
/**
* A cache frontend especially for the TYPO3 page cache
*
* It takes care of the fact that the array values "content" and "cache_data"
* of the passed array variable often contain redundant values and caches them
* separately.
*
* Each of those page-cache array values get stored into the same cache_pages
* cache backend but they get an identifier which is based on a hash of their
* content. So redundant entries are avoided.
*
*
* In the original page cache array those big data values which got redundancy
* squeezed are represented by an surrogate object "PageCacheEntry" whose sole
* purpose currently is to recognize them after unserialization and pull the
* additional data from the cache
*
*
* @author Bernhard Kraft <kraft@webconsulting.at>
*/
use \TYPO3\CMS\Core\Utility\GeneralUtility;
class PageCacheFrontend extends \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend {
const STRING_PREFIX = 'string_';
const SERIALIZED_PREFIX = 'serialized_';
/**
* Constructs the cache
*
* @param string $identifier A identifier which describes this cache
* @param \TYPO3\CMS\Core\Cache\Backend\BackendInterface $backend Backend to be used for this cache
* @throws \InvalidArgumentException if the backend isn't redundancy aware
*/
public function __construct($identifier, \TYPO3\CMS\Core\Cache\Backend\BackendInterface $backend) {
if (! $backend instanceof \TYPO3\CMS\Core\Cache\Backend\RedundancyAwareInterface) {
throw new \InvalidArgumentException('Backend class "' . get_class($backend) . '" does not implement "\TYPO3\CMS\Core\Cache\Backend\RedundancyAwareInterface".', 1391214304);
}
parent::__construct($identifier, $backend);
}
/**
* Saves the passed TYPO3 page cache array in the cache.
*
* @param string $entryIdentifier An identifier used for this cache entry
* @param array $variable The variable to cache
* @param array $tags Tags to associate with this cache entry
* @param integer $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
* @return void
* @throws \InvalidArgumentException if the identifier or tag is not valid
* @api
*/
public function set($entryIdentifier, $variable, array $tags = array(), $lifetime = NULL) {
if (!is_array($variable)) {
throw new \InvalidArgumentException('Only array variables supported for storing!', 1391168251);
}
// By default two array values ("content" and "cache_data") get stored on their own because they
// usually contain large blocks of data and are redundant among requests in many cases.
// Field "content" is redundant among cached page variants when logged in users see the same
// as non-logged in users like when no output altering conditions, no access restricted
// content elemnts, etc. are contained on a page.
$variable['content'] = $this->getDataSurrogate($variable['content']);
if ($variable['content'] instanceof \TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry) {
// A page cache entry remembers the redundant data pieces it uses also by adding
// appropriate tags. This allows to handle removal and garbage collection by the
// redundancy aware backend.
$tags[] = $variable['content']->getIdentifier();
}
// Field "cache_data" is redundant among every cached pages as long as they share the same
// TypoScript setup. The only disturbing factor is the array key "rootLine" which is of
// course different for every page. So move this array key to the cache variable itself.
$variable['cache_rootLine'] = $this->getDataSurrogate($variable['cache_data']['rootLine']);
if ($variable['cache_rootLine'] instanceof \TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry) {
$tags[] = $variable['cache_rootLine']->getIdentifier();
}
unset($variable['cache_data']['rootLine']);
$variable['cache_data'] = $this->getDataSurrogate($variable['cache_data']);
if ($variable['cache_data'] instanceof \TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry) {
$tags[] = $variable['cache_data']->getIdentifier();
}
// Store processed array
parent::set($entryIdentifier, $variable, $tags, $lifetime);
}
/**
* Finds and returns a variable value from the cache.
*
* @param string $entryIdentifier Identifier of the cache entry to fetch
* @return mixed The value
* @throws \InvalidArgumentException if the identifier is not valid
* @api
*/
public function get($entryIdentifier) {
$result = parent::get($entryIdentifier);
if (is_array($result)) {
// Restore all array values which were replaced by surrogates
return $this->getPageCacheOriginalData($result);
} else {
return FALSE;
}
}
/**
* Finds and returns all cache entries which are tagged by the specified tag.
*
* @param string $tag The tag to search for
* @return array An array with the content of all matching entries. An empty array if no entries matched
* @throws \InvalidArgumentException if the tag is not valid
* @api
*/
public function getByTag($tag) {
$entries = parent::getByTag($tag);
foreach ($entries as $key => $entry) {
// Restore all array values which were put in a cache entry on their own
$entries[$key] = $this->getPageCacheOriginalData($entry);
}
return $entries;
}
/**********************************************
*
* End of public API
*
**********************************************/
/**
* Determines if it is worth to put the data into the cache on its own.
* If the size of the (serialzed) data is more than 1kB this will happen.
* Data gets stored into cache by generating a hash identifier from the data.
* This results in avoiding redundant data in cache entries.
*
* @param mixed $data The data which should get stored
* @return mixed/array Either the original data if it was shorter than 1kB or an PageCacheArray data surrogate
*/
protected function getDataSurrogate($data) {
$prefix = self::STRING_PREFIX;
$serialized = false;
if (!is_string($data)) {
$prefix = self::SERIALIZED_PREFIX;
if ($this->useIgBinary === TRUE) {
$serialized = igbinary_serialize($data);
} else {
$serialized = serialize($data);
}
}
// If the serialized (or plain) data was less than 1024 characters (1024) don't
// store them on their own. Eventually the overhead is more than its worth.
if (strlen($serialized ? $serialized : $data) < 1024) {
return $data;
}
// Until this point we needed the original data to eventually return it.
// From now on only the value to be stores will be needed.
$data = $serialized ? $serialized : $data;
$hash = sha1($data);
$identifier = $prefix.$hash;
if (!$this->backend->has($identifier)) {
$this->backend->set($identifier, $data, array(), 0);
}
return GeneralUtility::makeInstance('TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry', $identifier);
}
/**
* Retrieves the contents of a redundant data surrogate which was put into the cache
*
* @param mixed,\TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry If an PageCacheEntry object (surrogate) is passed the associated data will get retrieved.
* @return mixed The original contents of the page cache array value
*/
protected function getOriginalData($content) {
if (! $content instanceof \TYPO3\CMS\Core\Cache\Frontend\PageCacheEntry) {
return $content;
}
$data = $this->backend->get($content->getIdentifier());
if (substr($content->getIdentifier(), 0, strlen(self::SERIALIZED_PREFIX)) === self::SERIALIZED_PREFIX) {
if ($this->useIgBinary === TRUE) {
$data = igbinary_unserialize($data);
} else {
$data = unserialize($data);
}
}
return $data;
}
/**
* Restores redundant page cache array values which were put in another cache entry
*
* @param array $variable The main cache entry of the page with eventual redundancy surrogates
* @return array $variable The same cache entry with all redundancy surrogates replaced properly
*/
protected function getPageCacheOriginalData(array $variable) {
if (isset($variable['content'])) {
$variable['content'] = $this->getOriginalData($variable['content']);
}
if (isset($variable['cache_data'])) {
$variable['cache_data'] = $this->getOriginalData($variable['cache_data']);
}
if (isset($variable['cache_rootLine'])) {
$variable['cache_data']['rootLine'] = $this->getOriginalData($variable['cache_rootLine']);
unset($variable['cache_rootLine']);
}
return $variable;
}
}
typo3/sysext/core/Configuration/DefaultConfiguration.php
'groups' => array('pages', 'all')
),
'cache_pages' => array(
'frontend' => 'TYPO3\CMS\Core\Cache\Frontend\VariableFrontend',
'backend' => 'TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend',
'frontend' => 'TYPO3\CMS\Core\Cache\Frontend\PageCacheFrontend',
'backend' => 'TYPO3\CMS\Core\Cache\Backend\PageCacheBackend',
'options' => array(
'compression' => TRUE
),
(1-1/2)