Task #55541 » pageCacheRedundancy.diff
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
|
||
),
|