Project

General

Profile

Bug #28514 » class.t3lib_cache_backend_memcachedbackend.php

Raphaël Riel, 2011-07-27 15:04

 
<?php
/***************************************************************
* Copyright notice
*
* (c) 2009-2011 Ingo Renner <ingo@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.
*
* 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 which stores cache entries by using Memcached.
*
* This backend uses the following types of Memcache keys:
* - tag_xxx
* xxx is tag name, value is array of associated identifiers identifier. This
* is "forward" tag index. It is mainly used for obtaining content by tag
* (get identifier by tag -> get content by identifier)
* - ident_xxx
* xxx is identifier, value is array of associated tags. This is "reverse" tag
* index. It provides quick access for all tags associated with this identifier
* and used when removing the identifier
*
* Each key is prepended with a prefix. By default prefix consists from two parts
* separated by underscore character and ends in yet another underscore character:
* - "TYPO3"
* - Current site path obtained from the PATH_site constant
* This prefix makes sure that keys from the different installations do not
* conflict.
*
* Note: When using the Memcached backend to store values of more than ~1 MB,
* the data will be split into chunks to make them fit into the memcached limits.
*
* This file is a backport from FLOW3 by Ingo Renner.
*
* @package TYPO3
* @subpackage t3lib_cache
* @api
* @version $Id: class.t3lib_cache_backend_memcachedbackend.php 10444 2011-02-11 22:48:05Z lolli $
*/
class t3lib_cache_backend_MemcachedBackend extends t3lib_cache_backend_AbstractBackend {

/**
* Max bucket size, (1024*1024)-42 bytes
* @var int
*/
const MAX_BUCKET_SIZE = 1048534;

/**
* Instance of the PHP Memcache class
*
* @var Memcache
*/
protected $memcache;

/**
* Array of Memcache server configurations
*
* @var array
*/
protected $servers = array();

/**
* Indicates whether the memcache uses compression or not (requires zlib),
* either 0 or MEMCACHE_COMPRESSED
*
* @var int
*/
protected $flags;

/**
* A prefix to seperate stored data from other data possibly stored in the
* memcache. This prefix must be unique for each site in the tree. Default
* implementation uses MD5 of the current site path to make identifier prefix
* unique.
*
* @var string
*/
protected $identifierPrefix;

/**
* Indicates whther the server is connected
*
* @var boolean
*/
protected $serverConnected = false;

/**
* Constructs this backend
*
* @param array $options Configuration options - depends on the actual backend
* @author Robert Lemke <robert@typo3.org>
*/
public function __construct(array $options = array()) {
if (!extension_loaded('memcache')) {
throw new t3lib_cache_Exception(
'The PHP extension "memcache" must be installed and loaded in ' .
'order to use the Memcached backend.',
1213987706
);
}

parent::__construct($options);

$this->memcache = new Memcache();
$defaultPort = ini_get('memcache.default_port');

if (!count($this->servers)) {
throw new t3lib_cache_Exception(
'No servers were given to Memcache',
1213115903
);
}

foreach ($this->servers as $serverConfiguration) {
if (substr($serverConfiguration, 0, 7) == 'unix://') {
$host = $serverConfiguration;
$port = 0;
} else {
if (substr($serverConfiguration, 0, 6) === 'tcp://') {
$serverConfiguration = substr($serverConfiguration, 6);
}
if (strstr($serverConfiguration, ':') !== FALSE) {
list($host, $port) = explode(':', $serverConfiguration, 2);
} else {
$host = $serverConfiguration;
$port = $defaultPort;
}
}

if ($this->serverConnected) {
$this->memcache->addserver($host, $port);
} else {
// pconnect throws PHP warnings when it cannot connect!
$this->serverConnected = @$this->memcache->pconnect($host, $port);
}
}

if (!$this->serverConnected) {
t3lib_div::sysLog('Unable to connect to any Memcached server', 'core', 3);
}
}

/**
* Setter for servers to be used. Expects an array, the values are expected
* to be formatted like "<host>[:<port>]" or "unix://<path>"
*
* @param array An array of servers to add.
* @return void
* @author Christian Jul Jensen <julle@typo3.org>
*/
protected function setServers(array $servers) {
$this->servers = $servers;
}

/**
* Setter for compression flags bit
*
* @param boolean $useCompression
* @return void
* @author Christian Jul Jensen <julle@typo3.org>
*/
protected function setCompression($useCompression) {
if ($useCompression === TRUE) {
$this->flags ^= MEMCACHE_COMPRESSED;
} else {
$this->flags &= ~MEMCACHE_COMPRESSED;
}
}

/**
* Initializes the identifier prefix when setting the cache.
*
* @param t3lib_cache_frontend_Frontend $cache The frontend for this backend
* @return void
* @author Robert Lemke <robert@typo3.org>
* @author Dmitry Dulepov
*/
public function setCache(t3lib_cache_frontend_Frontend $cache) {
parent::setCache($cache);
$this->identifierPrefix = 'TYPO3_' . md5(PATH_site) . '_';
}

/**
* Saves data in the cache.
*
* @param string An identifier for this specific cache entry
* @param string The data to be stored
* @param array Tags to associate with this cache entry
* @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
* @return void
* @throws t3lib_cache_Exception if no cache frontend has been set.
* @throws InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
* @throws t3lib_cache_exception_InvalidData if $data is not a string
* @author Christian Jul Jensen <julle@typo3.org>
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
throw new InvalidArgumentException(
'Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').',
1235839340
);
}

if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
throw new t3lib_cache_Exception(
'No cache frontend has been set yet via setCache().',
1207149215
);
}

if (!is_string($data)) {
throw new t3lib_cache_Exception_InvalidData(
'The specified data is of type "' . gettype($data) .
'" but a string is expected.',
1207149231
);
}

$tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
$expiration = $lifetime !== NULL ? $lifetime : $this->defaultLifetime;

// Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
// thus $expiration should be converted from lifetime to UNIX timestamp
if ($expiration > 2592000) {
$expiration += $GLOBALS['EXEC_TIME'];
}

try {
if (strlen($data) > self::MAX_BUCKET_SIZE) {
$data = str_split($data, 1024 * 1000);
$success = TRUE;
$chunkNumber = 1;

foreach ($data as $chunk) {
$this->debug("SET set() chunk ...");
$success = $success && $this->memcache->set(
$this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber,
$chunk,
$this->flags,
$expiration
);
$chunkNumber++;
}
$this->debug("SET set() chunk result");
$success = $success && $this->memcache->set(
$this->identifierPrefix . $entryIdentifier,
'TYPO3*chunked:' . $chunkNumber,
$this->flags,
$expiration
);
} else {
$this->debug("SET set() normal $entryIdentifier");
$success = $this->memcache->set(
$this->identifierPrefix . $entryIdentifier,
$data,
$this->flags,
$expiration
);
}

if ($success === TRUE) {
$this->removeIdentifierFromAllTags($entryIdentifier);
$this->addIdentifierToTags($entryIdentifier, $tags);
} else {
throw new t3lib_cache_Exception(
'Could not set data to memcache server.',
1275830266
);
}
} catch (Exception $exception) {
throw new t3lib_cache_Exception(
'Could not set value. ' .
$exception->getMessage(),
1207208100
);
}
}

/**
* Loads data from the cache.
*
* @param string An identifier which describes the cache entry to load
* @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
* @author Christian Jul Jensen <julle@typo3.org>
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function get($entryIdentifier) {
$this->debug("GET get() $entryIdentifier");
$value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);

if (substr($value, 0, 14) === 'TYPO3*chunked:') {
list(, $chunkCount) = explode(':', $value);
$value = '';

for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
$this->debug("GET get() $chunkNumber chunk");
$value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
}
}

return $value;
}

/**
* Checks if a cache entry with the specified identifier exists.
*
* @param string An identifier specifying the cache entry
* @return boolean TRUE if such an entry exists, FALSE if not
* @author Christian Jul Jensen <julle@typo3.org>
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function has($entryIdentifier) {
$this->debug("GET has() $entryIdentifier");
return $this->serverConnected && $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
}

/**
* Removes all cache entries matching the specified identifier.
* Usually this only affects one entry but if - for what reason ever -
* old entries for the identifier still exist, they are removed as well.
*
* @param string Specifies the cache entry to remove
* @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
* @author Christian Jul Jensen <julle@typo3.org>
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function remove($entryIdentifier) {
$this->removeIdentifierFromAllTags($entryIdentifier);
$this->debug("DELETE remove() $entryIdentifier");
return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
}

/**
* Finds and returns all cache entry identifiers which are tagged by the
* specified tag.
*
* @param string The tag to search for
* @return array An array of entries with all matching entries. An empty array if no entries matched
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function findIdentifiersByTag($tag) {
$this->debug("GET findIdentifiersByTag() $tag");
$identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);

if ($identifiers !== FALSE) {
return (array) $identifiers;
} else {
return array();
}
}


/**
* Finds and returns all cache entry identifiers which are tagged by the
* specified tags.
*
* @param array Array of tags to search for
* @return array An array with identifiers of all matching entries. An empty array if no entries matched
* @author Ingo Renner <ingo@typo3.org>
*/
public function findIdentifiersByTags(array $tags) {
$taggedEntries = array();
$foundEntries = array();

foreach ($tags as $tag) {
$taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
}

$intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);

foreach ($intersectedTaggedEntries as $entryIdentifier) {
$foundEntries[$entryIdentifier] = $entryIdentifier;
}

return $foundEntries;
}

/**
* Removes all cache entries of this cache.
*
* @return void
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function flush() {
if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1204111376);
}

$this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
}

/**
* 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
* @author Karsten Dambekalns <karsten@typo3.org>
*/
public function flushByTag($tag) {
$identifiers = $this->findIdentifiersByTag($tag);

foreach ($identifiers as $identifier) {
$this->remove($identifier);
}
}


/**
* Removes all cache entries of this cache which are tagged by the specified tag.
*
* @param array The tags the entries must have
* @return void
* @author Ingo Renner <ingo@typo3.org>
*/
public function flushByTags(array $tags) {
foreach ($tags as $tag) {
$this->flushByTag($tag);
}
}

/**
* Associates the identifier with the given tags
*
* @param string $entryIdentifier
* @param array Array of tags
* @author Karsten Dambekalns <karsten@typo3.org>
* @author Dmitry Dulepov <dmitry@typo3.org>
* @internal
*/
protected function addIdentifierToTags($entryIdentifier, array $tags) {
if ($this->serverConnected) {
foreach ($tags as $tag) {
// Update tag-to-identifier index
$identifiers = $this->findIdentifiersByTag($tag);
if (array_search($entryIdentifier, $identifiers) === false) {
$identifiers[] = $entryIdentifier;
$this->debug("SET addIdentifierToTags() $entryIdentifier");
$this->memcache->set($this->identifierPrefix . 'tag_' . $tag,
$identifiers);
}

// Update identifier-to-tag index
$existingTags = $this->findTagsByIdentifier($entryIdentifier);
if (array_search($tag, $existingTags) === FALSE) {
$this->debug("SET addIdentifierToTags() $entryIdentifier");
$this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier,
array_merge($existingTags, $tags));
}
}
}
}

/**
* Removes association of the identifier with the given tags
*
* @param string $entryIdentifier
* @param array Array of tags
* @author Karsten Dambekalns <karsten@typo3.org>
* @author Dmitry Dulepov <dmitry@typo3.org>
* @internal
*/
protected function removeIdentifierFromAllTags($entryIdentifier) {
if ($this->serverConnected) {
// Get tags for this identifier
$tags = $this->findTagsByIdentifier($entryIdentifier);
// Deassociate tags with this identifier
foreach ($tags as $tag) {
$identifiers = $this->findIdentifiersByTag($tag);
// Formally array_search() below should never return false
// due to the behavior of findTagsForIdentifier(). But if
// reverse index is corrupted, we still can get 'false' from
// array_search(). This is not a problem because we are
// removing this identifier from anywhere.
if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
unset($identifiers[$key]);

if (count($identifiers)) {
$this->debug("SET removeIdentifierFromAllTags() $entryIdentifier");
$this->memcache->set(
$this->identifierPrefix . 'tag_' . $tag,
$identifiers
);
} else {
$this->debug("DELETE removeIdentifierFromAllTags() $entryIdentifier");
$this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
}
}
}

// Clear reverse tag index for this identifier
$this->debug("DELETE removeIdentifierFromAllTags() $entryIdentifier");
$this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
}
}

/**
* Finds all tags for the given identifier. This function uses reverse tag
* index to search for tags.
*
* @param string Identifier to find tags by
* @return array Array with tags
* @author Dmitry Dulepov <dmitry@typo3.org>
* @internal
*/
protected function findTagsByIdentifier($identifier) {
$this->debug("GET findTagsByIdentifier() $identifier");
$tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
return ($tags === FALSE ? array() : (array) $tags);
}

/**
* Does nothing, as memcached does GC itself
*
* @return void
*/
public function collectGarbage() {
}


private function debug($log){
error_log("MEM_TYPO3_" . $log);
}

}


if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php'])) {
include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']);
}

?>
(2-2/2)