TYPO3 API  SVNRelease
class.t3lib_cache_backend_memcachedbackend.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003  *  Copyright notice
00004  *
00005  *  (c) 2009-2011 Ingo Renner <ingo@typo3.org>
00006  *  All rights reserved
00007  *
00008  *  This script is part of the TYPO3 project. The TYPO3 project is
00009  *  free software; you can redistribute it and/or modify
00010  *  it under the terms of the GNU General Public License as published by
00011  *  the Free Software Foundation; either version 2 of the License, or
00012  *  (at your option) any later version.
00013  *
00014  *  The GNU General Public License can be found at
00015  *  http://www.gnu.org/copyleft/gpl.html.
00016  *
00017  *  This script is distributed in the hope that it will be useful,
00018  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00019  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020  *  GNU General Public License for more details.
00021  *
00022  *  This copyright notice MUST APPEAR in all copies of the script!
00023  ***************************************************************/
00024 
00025 
00026 /**
00027  * A caching backend which stores cache entries by using Memcached.
00028  *
00029  * This backend uses the following types of Memcache keys:
00030  * - tag_xxx
00031  *   xxx is tag name, value is array of associated identifiers identifier. This
00032  *   is "forward" tag index. It is mainly used for obtaining content by tag
00033  *   (get identifier by tag -> get content by identifier)
00034  * - ident_xxx
00035  *   xxx is identifier, value is array of associated tags. This is "reverse" tag
00036  *   index. It provides quick access for all tags associated with this identifier
00037  *   and used when removing the identifier
00038  *
00039  * Each key is prepended with a prefix. By default prefix consists from two parts
00040  * separated by underscore character and ends in yet another underscore character:
00041  * - "TYPO3"
00042  * - Current site path obtained from the PATH_site constant
00043  * This prefix makes sure that keys from the different installations do not
00044  * conflict.
00045  *
00046  * Note: When using the Memcached backend to store values of more than ~1 MB,
00047  * the data will be split into chunks to make them fit into the memcached limits.
00048  *
00049  * This file is a backport from FLOW3 by Ingo Renner.
00050  *
00051  * @package TYPO3
00052  * @subpackage t3lib_cache
00053  * @api
00054  * @version $Id: class.t3lib_cache_backend_memcachedbackend.php 10443 2011-02-11 22:46:06Z lolli $
00055  */
00056 class t3lib_cache_backend_MemcachedBackend extends t3lib_cache_backend_AbstractBackend {
00057 
00058     /**
00059      * Max bucket size, (1024*1024)-42 bytes
00060      * @var int
00061      */
00062     const MAX_BUCKET_SIZE = 1048534;
00063 
00064     /**
00065      * Instance of the PHP Memcache class
00066      *
00067      * @var Memcache
00068      */
00069     protected $memcache;
00070 
00071     /**
00072      * Array of Memcache server configurations
00073      *
00074      * @var array
00075      */
00076     protected $servers = array();
00077 
00078     /**
00079      * Indicates whether the memcache uses compression or not (requires zlib),
00080      * either 0 or MEMCACHE_COMPRESSED
00081      *
00082      * @var int
00083      */
00084     protected $flags;
00085 
00086     /**
00087      * A prefix to seperate stored data from other data possibly stored in the
00088      * memcache. This prefix must be unique for each site in the tree. Default
00089      * implementation uses MD5 of the current site path to make identifier prefix
00090      * unique.
00091      *
00092      * @var string
00093      */
00094     protected $identifierPrefix;
00095 
00096     /**
00097      * Indicates whther the server is connected
00098      *
00099      * @var boolean
00100      */
00101     protected $serverConnected = false;
00102 
00103     /**
00104      * Constructs this backend
00105      *
00106      * @param array $options Configuration options - depends on the actual backend
00107      * @author Robert Lemke <robert@typo3.org>
00108      */
00109     public function __construct(array $options = array()) {
00110         if (!extension_loaded('memcache')) {
00111             throw new t3lib_cache_Exception(
00112                 'The PHP extension "memcache" must be installed and loaded in ' .
00113                 'order to use the Memcached backend.',
00114                 1213987706
00115             );
00116         }
00117 
00118         parent::__construct($options);
00119 
00120         $this->memcache = new Memcache();
00121         $defaultPort = ini_get('memcache.default_port');
00122 
00123         if (!count($this->servers)) {
00124             throw new t3lib_cache_Exception(
00125                 'No servers were given to Memcache',
00126                 1213115903
00127             );
00128         }
00129 
00130         foreach ($this->servers as $serverConfiguration) {
00131             if (substr($serverConfiguration, 0, 7) == 'unix://') {
00132                 $host = $serverConfiguration;
00133                 $port = 0;
00134             } else {
00135                 if (substr($serverConfiguration, 0, 6) === 'tcp://') {
00136                     $serverConfiguration = substr($serverConfiguration, 6);
00137                 }
00138                 if (strstr($serverConfiguration, ':') !== FALSE) {
00139                     list($host, $port) = explode(':', $serverConfiguration, 2);
00140                 } else {
00141                     $host = $serverConfiguration;
00142                     $port = $defaultPort;
00143                 }
00144             }
00145 
00146             if ($this->serverConnected) {
00147                 $this->memcache->addserver($host, $port);
00148             } else {
00149                     // pconnect throws PHP warnings when it cannot connect!
00150                 $this->serverConnected = @$this->memcache->pconnect($host, $port);
00151             }
00152         }
00153 
00154         if (!$this->serverConnected) {
00155             t3lib_div::sysLog('Unable to connect to any Memcached server', 'core', 3);
00156         }
00157     }
00158 
00159     /**
00160      * Setter for servers to be used. Expects an array,  the values are expected
00161      * to be formatted like "<host>[:<port>]" or "unix://<path>"
00162      *
00163      * @param   array   An array of servers to add.
00164      * @return  void
00165      * @author Christian Jul Jensen <julle@typo3.org>
00166      */
00167     protected function setServers(array $servers) {
00168         $this->servers = $servers;
00169     }
00170 
00171     /**
00172      * Setter for compression flags bit
00173      *
00174      * @param boolean $useCompression
00175      * @return void
00176      * @author Christian Jul Jensen <julle@typo3.org>
00177      */
00178     protected function setCompression($useCompression) {
00179         if ($useCompression === TRUE) {
00180             $this->flags ^= MEMCACHE_COMPRESSED;
00181         } else {
00182             $this->flags &= ~MEMCACHE_COMPRESSED;
00183         }
00184     }
00185 
00186     /**
00187      * Initializes the identifier prefix when setting the cache.
00188      *
00189      * @param t3lib_cache_frontend_Frontend $cache The frontend for this backend
00190      * @return void
00191      * @author Robert Lemke <robert@typo3.org>
00192      * @author Dmitry Dulepov
00193      */
00194     public function setCache(t3lib_cache_frontend_Frontend $cache) {
00195         parent::setCache($cache);
00196         $this->identifierPrefix = 'TYPO3_' . md5(PATH_site) . '_';
00197     }
00198 
00199     /**
00200      * Saves data in the cache.
00201      *
00202      * @param string An identifier for this specific cache entry
00203      * @param string The data to be stored
00204      * @param array Tags to associate with this cache entry
00205      * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
00206      * @return void
00207      * @throws t3lib_cache_Exception if no cache frontend has been set.
00208      * @throws InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
00209      * @throws t3lib_cache_exception_InvalidData if $data is not a string
00210      * @author Christian Jul Jensen <julle@typo3.org>
00211      * @author Karsten Dambekalns <karsten@typo3.org>
00212      */
00213     public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
00214         if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
00215             throw new InvalidArgumentException(
00216                 'Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').',
00217                 1235839340
00218             );
00219         }
00220 
00221         if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
00222             throw new t3lib_cache_Exception(
00223                 'No cache frontend has been set yet via setCache().',
00224                 1207149215
00225             );
00226         }
00227 
00228         if (!is_string($data)) {
00229             throw new t3lib_cache_Exception_InvalidData(
00230                 'The specified data is of type "' . gettype($data) .
00231                 '" but a string is expected.',
00232                 1207149231
00233             );
00234         }
00235 
00236         $tags[] = '%MEMCACHEBE%' . $this->cacheIdentifier;
00237         $expiration = $lifetime !== NULL ? $lifetime : $this->defaultLifetime;
00238 
00239             // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
00240             // thus $expiration should be converted from lifetime to UNIX timestamp
00241         if ($expiration > 2592000) {
00242             $expiration += $GLOBALS['EXEC_TIME'];
00243         }
00244 
00245         try {
00246             if (strlen($data) > self::MAX_BUCKET_SIZE) {
00247                 $data = str_split($data, 1024 * 1000);
00248                 $success = TRUE;
00249                 $chunkNumber = 1;
00250 
00251                 foreach ($data as $chunk) {
00252                     $success = $success && $this->memcache->set(
00253                         $this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber,
00254                         $chunk,
00255                         $this->flags,
00256                         $expiration
00257                     );
00258                     $chunkNumber++;
00259                 }
00260                 $success = $success && $this->memcache->set(
00261                     $this->identifierPrefix . $entryIdentifier,
00262                     'TYPO3*chunked:' . $chunkNumber,
00263                     $this->flags,
00264                     $expiration
00265                 );
00266             } else {
00267                 $success = $this->memcache->set(
00268                     $this->identifierPrefix . $entryIdentifier,
00269                     $data,
00270                     $this->flags,
00271                     $expiration
00272                 );
00273             }
00274 
00275             if ($success === TRUE) {
00276                 $this->removeIdentifierFromAllTags($entryIdentifier);
00277                 $this->addIdentifierToTags($entryIdentifier, $tags);
00278             } else {
00279                 throw new t3lib_cache_Exception(
00280                     'Could not set data to memcache server.',
00281                     1275830266
00282                 );
00283             }
00284         } catch (Exception $exception) {
00285             throw new t3lib_cache_Exception(
00286                 'Could not set value. ' .
00287                 $exception->getMessage(),
00288                 1207208100
00289             );
00290         }
00291     }
00292 
00293     /**
00294      * Loads data from the cache.
00295      *
00296      * @param string An identifier which describes the cache entry to load
00297      * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
00298      * @author Christian Jul Jensen <julle@typo3.org>
00299      * @author Karsten Dambekalns <karsten@typo3.org>
00300      */
00301     public function get($entryIdentifier) {
00302         $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
00303 
00304         if (substr($value, 0, 14) === 'TYPO3*chunked:') {
00305             list(, $chunkCount) = explode(':', $value);
00306             $value = '';
00307 
00308             for ($chunkNumber = 1; $chunkNumber < $chunkCount; $chunkNumber++) {
00309                 $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
00310             }
00311         }
00312 
00313         return $value;
00314     }
00315 
00316     /**
00317      * Checks if a cache entry with the specified identifier exists.
00318      *
00319      * @param string An identifier specifying the cache entry
00320      * @return boolean TRUE if such an entry exists, FALSE if not
00321      * @author Christian Jul Jensen <julle@typo3.org>
00322      * @author Karsten Dambekalns <karsten@typo3.org>
00323      */
00324     public function has($entryIdentifier) {
00325         return $this->serverConnected && $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
00326     }
00327 
00328     /**
00329      * Removes all cache entries matching the specified identifier.
00330      * Usually this only affects one entry but if - for what reason ever -
00331      * old entries for the identifier still exist, they are removed as well.
00332      *
00333      * @param string Specifies the cache entry to remove
00334      * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
00335      * @author Christian Jul Jensen <julle@typo3.org>
00336      * @author Karsten Dambekalns <karsten@typo3.org>
00337      */
00338     public function remove($entryIdentifier) {
00339         $this->removeIdentifierFromAllTags($entryIdentifier);
00340         return $this->memcache->delete($this->identifierPrefix . $entryIdentifier, 0);
00341     }
00342 
00343     /**
00344      * Finds and returns all cache entry identifiers which are tagged by the
00345      * specified tag.
00346      *
00347      * @param string The tag to search for
00348      * @return array An array of entries with all matching entries. An empty array if no entries matched
00349      * @author Karsten Dambekalns <karsten@typo3.org>
00350      */
00351     public function findIdentifiersByTag($tag) {
00352         $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
00353 
00354         if ($identifiers !== FALSE) {
00355             return (array) $identifiers;
00356         } else {
00357             return array();
00358         }
00359     }
00360 
00361 
00362     /**
00363      * Finds and returns all cache entry identifiers which are tagged by the
00364      * specified tags.
00365      *
00366      * @param array Array of tags to search for
00367      * @return array An array with identifiers of all matching entries. An empty array if no entries matched
00368      * @author Ingo Renner <ingo@typo3.org>
00369      */
00370     public function findIdentifiersByTags(array $tags) {
00371         $taggedEntries = array();
00372         $foundEntries = array();
00373 
00374         foreach ($tags as $tag) {
00375             $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
00376         }
00377 
00378         $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
00379 
00380         foreach ($intersectedTaggedEntries as $entryIdentifier) {
00381             $foundEntries[$entryIdentifier] = $entryIdentifier;
00382         }
00383 
00384         return $foundEntries;
00385     }
00386 
00387     /**
00388      * Removes all cache entries of this cache.
00389      *
00390      * @return void
00391      * @author Karsten Dambekalns <karsten@typo3.org>
00392      */
00393     public function flush() {
00394         if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
00395             throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1204111376);
00396         }
00397 
00398         $this->flushByTag('%MEMCACHEBE%' . $this->cacheIdentifier);
00399     }
00400 
00401     /**
00402      * Removes all cache entries of this cache which are tagged by the specified tag.
00403      *
00404      * @param string $tag The tag the entries must have
00405      * @return void
00406      * @author Karsten Dambekalns <karsten@typo3.org>
00407      */
00408     public function flushByTag($tag) {
00409         $identifiers = $this->findIdentifiersByTag($tag);
00410 
00411         foreach ($identifiers as $identifier) {
00412             $this->remove($identifier);
00413         }
00414     }
00415 
00416 
00417     /**
00418      * Removes all cache entries of this cache which are tagged by the specified tag.
00419      *
00420      * @param array The tags the entries must have
00421      * @return void
00422      * @author Ingo Renner <ingo@typo3.org>
00423      */
00424     public function flushByTags(array $tags) {
00425         foreach ($tags as $tag) {
00426             $this->flushByTag($tag);
00427         }
00428     }
00429 
00430     /**
00431      * Associates the identifier with the given tags
00432      *
00433      * @param string $entryIdentifier
00434      * @param array Array of tags
00435      * @author Karsten Dambekalns <karsten@typo3.org>
00436      * @author  Dmitry Dulepov <dmitry@typo3.org>
00437      * @internal
00438      */
00439     protected function addIdentifierToTags($entryIdentifier, array $tags) {
00440         if ($this->serverConnected) {
00441             foreach ($tags as $tag) {
00442                     // Update tag-to-identifier index
00443                 $identifiers = $this->findIdentifiersByTag($tag);
00444                 if (array_search($entryIdentifier, $identifiers) === false) {
00445                     $identifiers[] = $entryIdentifier;
00446                     $this->memcache->set($this->identifierPrefix . 'tag_' . $tag,
00447                                          $identifiers);
00448                 }
00449 
00450                     // Update identifier-to-tag index
00451                 $existingTags = $this->findTagsByIdentifier($entryIdentifier);
00452                 if (array_search($tag, $existingTags) === FALSE) {
00453                     $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier,
00454                                          array_merge($existingTags, $tags));
00455                 }
00456             }
00457         }
00458     }
00459 
00460     /**
00461      * Removes association of the identifier with the given tags
00462      *
00463      * @param string $entryIdentifier
00464      * @param array Array of tags
00465      * @author Karsten Dambekalns <karsten@typo3.org>
00466      * @author  Dmitry Dulepov <dmitry@typo3.org>
00467      * @internal
00468      */
00469     protected function removeIdentifierFromAllTags($entryIdentifier) {
00470         if ($this->serverConnected) {
00471                 // Get tags for this identifier
00472             $tags = $this->findTagsByIdentifier($entryIdentifier);
00473                 // Deassociate tags with this identifier
00474             foreach ($tags as $tag) {
00475                 $identifiers = $this->findIdentifiersByTag($tag);
00476                     // Formally array_search() below should never return false
00477                     // due to the behavior of findTagsForIdentifier(). But if
00478                     // reverse index is corrupted, we still can get 'false' from
00479                     // array_search(). This is not a problem because we are
00480                     // removing this identifier from anywhere.
00481                 if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
00482                     unset($identifiers[$key]);
00483 
00484                     if (count($identifiers)) {
00485                         $this->memcache->set(
00486                             $this->identifierPrefix . 'tag_' . $tag,
00487                             $identifiers
00488                         );
00489                     } else {
00490                         $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag, 0);
00491                     }
00492                 }
00493             }
00494 
00495                 // Clear reverse tag index for this identifier
00496             $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier, 0);
00497         }
00498     }
00499 
00500     /**
00501      * Finds all tags for the given identifier. This function uses reverse tag
00502      * index to search for tags.
00503      *
00504      * @param   string  Identifier to find tags by
00505      * @return  array   Array with tags
00506      * @author Dmitry Dulepov <dmitry@typo3.org>
00507      * @internal
00508      */
00509     protected function findTagsByIdentifier($identifier) {
00510         $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
00511         return ($tags === FALSE ? array() : (array) $tags);
00512     }
00513 
00514     /**
00515      * Does nothing, as memcached does GC itself
00516      *
00517      * @return void
00518      */
00519     public function collectGarbage() {
00520     }
00521 }
00522 
00523 
00524 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php'])) {
00525     include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']);
00526 }
00527 
00528 ?>