Updated the caching framework to the according FLOW3 revision 2616
[Packages/TYPO3.CMS.git] / t3lib / cache / backend / class.t3lib_cache_backend_memcachedbackend.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Ingo Renner <ingo@typo3.org>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
24
25
26 /**
27 * A caching backend which stores cache entries by using Memcached.
28 *
29 * This backend uses the following types of Memcache keys:
30 * - tag_xxx
31 * xxx is tag name, value is array of associated identifiers identifier. This
32 * is "forward" tag index. It is mainly used for obtaining content by tag
33 * (get identifier by tag -> get content by identifier)
34 * - ident_xxx
35 * xxx is identifier, value is array of associated tags. This is "reverse" tag
36 * index. It provides quick access for all tags associated with this identifier
37 * and used when removing the identifier
38 * - tagIndex
39 * Value is a List of all tags (array)
40 * Each key is prepended with a prefix. By default prefix consists from two parts
41 * separated by underscore character and ends in yet another underscore character:
42 * - "TYPO3"
43 * - Current site path obtained from the PATH_site constant
44 * This prefix makes sure that keys from the different installations do not
45 * conflict.
46 *
47 * Note: When using the Memcached backend to store values of more than ~1 MB,
48 * the data will be split into chunks to make them fit into the memcached limits.
49 *
50 * This file is a backport from FLOW3 by Ingo Renner.
51 *
52 * @package TYPO3
53 * @subpackage t3lib_cache
54 * @version $Id$
55 */
56 class t3lib_cache_backend_MemcachedBackend extends t3lib_cache_backend_AbstractBackend {
57
58 /**
59 * Max bucket size, (1024*1024)-42 bytes
60 * @var int
61 */
62 const MAX_BUCKET_SIZE = 1048534;
63
64 /**
65 * Instance of the PHP Memcache class
66 *
67 * @var Memcache
68 */
69 protected $memcache;
70
71 /**
72 * Array of Memcache server configurations
73 *
74 * @var array
75 */
76 protected $servers = array();
77
78 /**
79 * Indicates whether the memcache uses compression or not (requires zlib),
80 * either 0 or MEMCACHE_COMPRESSED
81 *
82 * @var int
83 */
84 protected $flags;
85
86 /**
87 * A prefix to seperate stored data from other data possibly stored in the
88 * memcache. This prefix must be unique for each site in the tree. Default
89 * implementation uses MD5 of the current site path to make identifier prefix
90 * unique.
91 *
92 * @var string
93 */
94 protected $identifierPrefix;
95
96 /**
97 * Indicates whther the server is connected
98 *
99 * @var boolean
100 */
101 protected $serverConnected = false;
102
103 /**
104 * Constructs this backend
105 *
106 * @param mixed $options Configuration options - depends on the actual backend
107 * @author Robert Lemke <robert@typo3.org>
108 */
109 public function __construct($options = array()) {
110 if (!extension_loaded('memcache')) {
111 throw new t3lib_cache_Exception(
112 'The PHP extension "memcached" must be installed and loaded in ' .
113 'order to use the Memcached backend.',
114 1213987706
115 );
116 }
117
118 parent::__construct($options);
119
120 $this->memcache = new Memcache();
121 $this->identifierPrefix = $this->getIdentifierPrefix();
122 $defaultPort = ini_get('memcache.default_port');
123
124 if (!count($this->servers)) {
125 throw new t3lib_cache_Exception(
126 'No servers were given to Memcache',
127 1213115903
128 );
129 }
130
131 foreach ($this->servers as $serverConfiguration) {
132 if (substr($serverConfiguration, 0, 7) == 'unix://') {
133 $host = $serverConfiguration;
134 $port = 0;
135 } else {
136 if (substr($serverConfiguration, 0, 6) === 'tcp://') {
137 $serverConfiguration = substr($serverConfiguration, 6);
138 }
139 if (strstr($serverConfiguration, ':') !== FALSE) {
140 list($host, $port) = explode(':', $serverConfiguration, 2);
141 } else {
142 $host = $serverConfiguration;
143 $port = $defaultPort;
144 }
145 }
146
147 if ($this->serverConnected) {
148 $this->memcache->addserver($host, $port);
149 } else {
150 // pconnect throws PHP warnings when it cannot connect!
151 $this->serverConnected = @$this->memcache->pconnect($host, $port);
152 }
153 }
154
155 if (!$this->serverConnected) {
156 t3lib_div::sysLog('Unable to connect to any Memcached server', 'core', 3);
157 }
158 }
159
160 /**
161 * Setter for servers to be used. Expects an array, the values are expected
162 * to be formatted like "<host>[:<port>]" or "unix://<path>"
163 *
164 * @param array An array of servers to add.
165 * @return void
166 * @author Christian Jul Jensen <julle@typo3.org>
167 */
168 protected function setServers(array $servers) {
169 $this->servers = $servers;
170 }
171
172 /**
173 * Setter for compression flags bit
174 *
175 * @param boolean $useCompression
176 * @return void
177 * @author Christian Jul Jensen <julle@typo3.org>
178 */
179 protected function setCompression($useCompression) {
180 if ($useCompression === TRUE) {
181 $this->flags ^= MEMCACHE_COMPRESSED;
182 } else {
183 $this->flags &= ~MEMCACHE_COMPRESSED;
184 }
185 }
186
187 /**
188 * Saves data in the cache.
189 *
190 * @param string An identifier for this specific cache entry
191 * @param string The data to be stored
192 * @param array Tags to associate with this cache entry
193 * @param integer Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
194 * @return void
195 * @throws t3lib_cache_Exception if no cache frontend has been set.
196 * @throws InvalidArgumentException if the identifier is not valid or the final memcached key is longer than 250 characters
197 * @throws t3lib_cache_exception_InvalidData if $data is not a string
198 * @author Christian Jul Jensen <julle@typo3.org>
199 * @author Karsten Dambekalns <karsten@typo3.org>
200 */
201 public function set($entryIdentifier, $data, array $tags = array(), $lifetime = NULL) {
202 if (strlen($this->identifierPrefix . $entryIdentifier) > 250) {
203 throw new InvalidArgumentException(
204 'Could not set value. Key more than 250 characters (' . $this->identifierPrefix . $entryIdentifier . ').',
205 1235839340
206 );
207 }
208
209 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
210 throw new t3lib_cache_Exception(
211 'No cache frontend has been set yet via setCache().',
212 1207149215
213 );
214 }
215
216 if (!is_string($data)) {
217 throw new t3lib_cache_Exception_InvalidData(
218 'The specified data is of type "' . gettype($data) .
219 '" but a string is expected.',
220 1207149231
221 );
222 }
223
224 $tags[] = '%MEMCACHEBE%' . $this->cache->getIdentifier();
225 $expiration = $lifetime !== NULL ? $lifetime : $this->defaultLifetime;
226
227 try {
228 if(strlen($data) > self::MAX_BUCKET_SIZE) {
229 $data = str_split($data, 1024 * 1000);
230 $success = TRUE;
231 $chunkNumber = 1;
232
233 foreach ($data as $chunk) {
234 $success &= $this->memcache->set(
235 $this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber,
236 $chunk,
237 $this->flags,
238 $expiration
239 );
240 $chunkNumber++;
241 }
242 $success &= $this->memcache->set(
243 $this->identifierPrefix . $entryIdentifier,
244 'TYPO3*chunked:' . $chunkNumber,
245 $this->flags,
246 $expiration
247 );
248 } else {
249 $success = $this->memcache->set(
250 $this->identifierPrefix . $entryIdentifier,
251 $data,
252 $this->flags,
253 $expiration
254 );
255 }
256
257 if ($success === TRUE) {
258 $this->removeIdentifierFromAllTags($entryIdentifier);
259 $this->addTagsToTagIndex($tags);
260 $this->addIdentifierToTags($entryIdentifier, $tags);
261 }
262 } catch(Exception $exception) {
263 throw new t3lib_cache_Exception(
264 'Could not set value. ' .
265 $exception->getMessage(),
266 1207208100
267 );
268 }
269 }
270
271 /**
272 * Loads data from the cache.
273 *
274 * @param string An identifier which describes the cache entry to load
275 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
276 * @author Christian Jul Jensen <julle@typo3.org>
277 * @author Karsten Dambekalns <karsten@typo3.org>
278 */
279 public function get($entryIdentifier) {
280 $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
281
282 if (substr($value, 0, 14) === 'TYPO3*chunked:') {
283 list( , $chunkCount) = explode(':', $value);
284 $value = '';
285
286 for ($chunkNumber = 1 ; $chunkNumber < $chunkCount; $chunkNumber++) {
287 $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
288 }
289 }
290
291 return $value;
292 }
293
294 /**
295 * Checks if a cache entry with the specified identifier exists.
296 *
297 * @param string An identifier specifying the cache entry
298 * @return boolean TRUE if such an entry exists, FALSE if not
299 * @author Christian Jul Jensen <julle@typo3.org>
300 * @author Karsten Dambekalns <karsten@typo3.org>
301 */
302 public function has($entryIdentifier) {
303 return $this->serverConnected && $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
304 }
305
306 /**
307 * Removes all cache entries matching the specified identifier.
308 * Usually this only affects one entry but if - for what reason ever -
309 * old entries for the identifier still exist, they are removed as well.
310 *
311 * @param string Specifies the cache entry to remove
312 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
313 * @author Christian Jul Jensen <julle@typo3.org>
314 * @author Karsten Dambekalns <karsten@typo3.org>
315 */
316 public function remove($entryIdentifier) {
317 $this->removeIdentifierFromAllTags($entryIdentifier);
318 return $this->memcache->delete($this->identifierPrefix . $entryIdentifier);
319 }
320
321 /**
322 * Finds and returns all cache entry identifiers which are tagged by the
323 * specified tag.
324 *
325 * @param string The tag to search for
326 * @return array An array of entries with all matching entries. An empty array if no entries matched
327 * @author Karsten Dambekalns <karsten@typo3.org>
328 */
329 public function findIdentifiersByTag($tag) {
330 $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
331
332 if ($identifiers !== FALSE) {
333 return (array) $identifiers;
334 } else {
335 return array();
336 }
337 }
338
339
340 /**
341 * Finds and returns all cache entry identifiers which are tagged by the
342 * specified tags.
343 *
344 * @param array Array of tags to search for
345 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
346 * @author Ingo Renner <ingo@typo3.org>
347 */
348 public function findIdentifiersByTags(array $tags) {
349 $taggedEntries = array();
350 $foundEntries = array();
351
352 foreach ($tags as $tag) {
353 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
354 }
355
356 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
357
358 foreach ($intersectedTaggedEntries as $entryIdentifier) {
359 $foundEntries[$entryIdentifier] = $entryIdentifier;
360 }
361
362 return $foundEntries;
363 }
364
365 /**
366 * Removes all cache entries of this cache.
367 *
368 * @return void
369 * @author Karsten Dambekalns <karsten@typo3.org>
370 */
371 public function flush() {
372 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
373 throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1204111376);
374 }
375
376 $this->flushByTag('%MEMCACHEBE%' . $this->cache->getIdentifier());
377 }
378
379 /**
380 * Removes all cache entries of this cache which are tagged by the specified tag.
381 *
382 * @param string $tag The tag the entries must have
383 * @return void
384 * @author Karsten Dambekalns <karsten@typo3.org>
385 */
386 public function flushByTag($tag) {
387 $identifiers = $this->findIdentifiersByTag($tag);
388
389 foreach($identifiers as $identifier) {
390 $this->remove($identifier);
391 }
392 }
393
394
395 /**
396 * Removes all cache entries of this cache which are tagged by the specified tag.
397 *
398 * @param array The tags the entries must have
399 * @return void
400 * @author Ingo Renner <ingo@typo3.org>
401 */
402 public function flushByTags(array $tags) {
403 foreach ($tags as $tag) {
404 $this->flushByTag($tag);
405 }
406 }
407
408 /**
409 * Returns an array with all known tags
410 *
411 * @return array
412 * @author Karsten Dambekalns <karsten@typo3.org>
413 * @internal
414 */
415 protected function getTagIndex() {
416 $tagIndex = $this->memcache->get($this->identifierPrefix . 'tagIndex');
417 return ($tagIndex == false ? array() : (array)$tagIndex);
418 }
419
420 /**
421 * Saves the tags known to the backend
422 *
423 * @param array Array of tags
424 * @author Karsten Dambekalns <karsten@typo3.org>
425 * @internal
426 */
427 protected function setTagIndex(array $tags) {
428 $this->memcache->set($this->identifierPrefix . 'tagIndex', array_unique($tags), 0, 0);
429 }
430
431 /**
432 * Adds the given tags to the tag index
433 *
434 * @param array Array of tags
435 * @return void
436 * @author Karsten Dambekalns <karsten@typo3.org>
437 * @internal
438 */
439 protected function addTagsToTagIndex(array $tags) {
440 if(count($tags)) {
441 $this->setTagIndex(array_merge($tags, $this->getTagIndex()));
442 }
443 }
444
445 /**
446 * Removes the given tags from the tag index
447 *
448 * @param array $tags
449 * @return void
450 * @author Karsten Dambekalns <karsten@typo3.org>
451 * @internal
452 */
453 protected function removeTagsFromTagIndex(array $tags) {
454 if(count($tags)) {
455 $this->setTagIndex(array_diff($this->getTagIndex(), $tags));
456 }
457 }
458
459 /**
460 * Associates the identifier with the given tags
461 *
462 * @param string $entryIdentifier
463 * @param array Array of tags
464 * @author Karsten Dambekalns <karsten@typo3.org>
465 * @author Dmitry Dulepov <dmitry@typo3.org>
466 * @internal
467 */
468 protected function addIdentifierToTags($entryIdentifier, array $tags) {
469 if ($this->serverConnected) {
470 foreach($tags as $tag) {
471 // Update tag-to-identifier index
472 $identifiers = $this->findIdentifiersByTag($tag);
473 if (array_search($entryIdentifier, $identifiers) === false) {
474 $identifiers[] = $entryIdentifier;
475 $this->memcache->set($this->identifierPrefix . 'tag_' . $tag,
476 $identifiers);
477 }
478
479 // Update identifier-to-tag index
480 $existingTags = $this->findTagsByIdentifier($entryIdentifier);
481 if (array_search($entryIdentifier, $existingTags) === false) {
482 $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier,
483 array_merge($existingTags, $tags));
484 }
485 }
486 }
487 }
488
489 /**
490 * Removes association of the identifier with the given tags
491 *
492 * @param string $entryIdentifier
493 * @param array Array of tags
494 * @author Karsten Dambekalns <karsten@typo3.org>
495 * @author Dmitry Dulepov <dmitry@typo3.org>
496 * @internal
497 */
498 protected function removeIdentifierFromAllTags($entryIdentifier) {
499 if ($this->serverConnected) {
500 // Get tags for this identifier
501 $tags = $this->findTagsByIdentifier($entryIdentifier);
502 // Deassociate tags with this identifier
503 foreach ($tags as $tag) {
504 $identifiers = $this->findIdentifiersByTag($tag);
505 // Formally array_search() below should never return false
506 // due to the behavior of findTagsForIdentifier(). But if
507 // reverse index is corrupted, we still can get 'false' from
508 // array_search(). This is not a problem because we are
509 // removing this identifier from anywhere.
510 if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
511 unset($identifiers[$key]);
512
513 if(count($identifiers)) {
514 $this->memcache->set(
515 $this->identifierPrefix . 'tag_' . $tag,
516 $identifiers
517 );
518 } else {
519 $this->removeTagsFromTagIndex(array($tag));
520 $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag);
521 }
522 }
523 }
524
525 // Clear reverse tag index for this identifier
526 $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier);
527 }
528 }
529
530 /**
531 * Finds all tags for the given identifier. This function uses reverse tag
532 * index to search for tags.
533 *
534 * @param string Identifier to find tags by
535 * @return array Array with tags
536 * @author Dmitry Dulepov <dmitry@typo3.org>
537 * @internal
538 */
539 protected function findTagsByIdentifier($identifier) {
540 $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
541 return ($tags === FALSE ? array() : (array)$tags);
542 }
543
544 /**
545 * Returns idenfier prefix. Extensions can override this function to provide
546 * another identifier prefix if it is necessary for special purposes.
547 * Default identifier prefix is based on PATH_site only. In most cases
548 * it is enough because different installations use different paths and page
549 * IDs in the same installation never repeat.
550 *
551 * @return string Identifier prefix, ending with underscore
552 * @author Dmitry Dulepov
553 */
554 protected function getIdentifierPrefix() {
555 return 'TYPO3_' . md5(PATH_site) . '_';
556 }
557
558 /**
559 * Does nothing, as memcached does GC itself
560 *
561 * @return void
562 */
563 public function collectGarbage() {
564 }
565 }
566
567
568 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']) {
569 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']);
570 }
571
572 ?>