Fixed bug #11326: Memcached backend does not work well if cache of a page shall expir...
[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 // Memcached consideres values over 2592000 sec (30 days) as UNIX timestamp
228 // thus $expiration should be converted from lifetime to UNIX timestamp
229 if ($expiration > 2592000) {
230 $expiration += $GLOBALS['EXEC_TIME'];
231 }
232
233 try {
234 if(strlen($data) > self::MAX_BUCKET_SIZE) {
235 $data = str_split($data, 1024 * 1000);
236 $success = TRUE;
237 $chunkNumber = 1;
238
239 foreach ($data as $chunk) {
240 $success &= $this->memcache->set(
241 $this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber,
242 $chunk,
243 $this->flags,
244 $expiration
245 );
246 $chunkNumber++;
247 }
248 $success &= $this->memcache->set(
249 $this->identifierPrefix . $entryIdentifier,
250 'TYPO3*chunked:' . $chunkNumber,
251 $this->flags,
252 $expiration
253 );
254 } else {
255 $success = $this->memcache->set(
256 $this->identifierPrefix . $entryIdentifier,
257 $data,
258 $this->flags,
259 $expiration
260 );
261 }
262
263 if ($success === TRUE) {
264 $this->removeIdentifierFromAllTags($entryIdentifier);
265 $this->addTagsToTagIndex($tags);
266 $this->addIdentifierToTags($entryIdentifier, $tags);
267 }
268 } catch(Exception $exception) {
269 throw new t3lib_cache_Exception(
270 'Could not set value. ' .
271 $exception->getMessage(),
272 1207208100
273 );
274 }
275 }
276
277 /**
278 * Loads data from the cache.
279 *
280 * @param string An identifier which describes the cache entry to load
281 * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
282 * @author Christian Jul Jensen <julle@typo3.org>
283 * @author Karsten Dambekalns <karsten@typo3.org>
284 */
285 public function get($entryIdentifier) {
286 $value = $this->memcache->get($this->identifierPrefix . $entryIdentifier);
287
288 if (substr($value, 0, 14) === 'TYPO3*chunked:') {
289 list( , $chunkCount) = explode(':', $value);
290 $value = '';
291
292 for ($chunkNumber = 1 ; $chunkNumber < $chunkCount; $chunkNumber++) {
293 $value .= $this->memcache->get($this->identifierPrefix . $entryIdentifier . '_chunk_' . $chunkNumber);
294 }
295 }
296
297 return $value;
298 }
299
300 /**
301 * Checks if a cache entry with the specified identifier exists.
302 *
303 * @param string An identifier specifying the cache entry
304 * @return boolean TRUE if such an entry exists, FALSE if not
305 * @author Christian Jul Jensen <julle@typo3.org>
306 * @author Karsten Dambekalns <karsten@typo3.org>
307 */
308 public function has($entryIdentifier) {
309 return $this->serverConnected && $this->memcache->get($this->identifierPrefix . $entryIdentifier) !== false;
310 }
311
312 /**
313 * Removes all cache entries matching the specified identifier.
314 * Usually this only affects one entry but if - for what reason ever -
315 * old entries for the identifier still exist, they are removed as well.
316 *
317 * @param string Specifies the cache entry to remove
318 * @return boolean TRUE if (at least) an entry could be removed or FALSE if no entry was found
319 * @author Christian Jul Jensen <julle@typo3.org>
320 * @author Karsten Dambekalns <karsten@typo3.org>
321 */
322 public function remove($entryIdentifier) {
323 $this->removeIdentifierFromAllTags($entryIdentifier);
324 return $this->memcache->delete($this->identifierPrefix . $entryIdentifier);
325 }
326
327 /**
328 * Finds and returns all cache entry identifiers which are tagged by the
329 * specified tag.
330 *
331 * @param string The tag to search for
332 * @return array An array of entries with all matching entries. An empty array if no entries matched
333 * @author Karsten Dambekalns <karsten@typo3.org>
334 */
335 public function findIdentifiersByTag($tag) {
336 $identifiers = $this->memcache->get($this->identifierPrefix . 'tag_' . $tag);
337
338 if ($identifiers !== FALSE) {
339 return (array) $identifiers;
340 } else {
341 return array();
342 }
343 }
344
345
346 /**
347 * Finds and returns all cache entry identifiers which are tagged by the
348 * specified tags.
349 *
350 * @param array Array of tags to search for
351 * @return array An array with identifiers of all matching entries. An empty array if no entries matched
352 * @author Ingo Renner <ingo@typo3.org>
353 */
354 public function findIdentifiersByTags(array $tags) {
355 $taggedEntries = array();
356 $foundEntries = array();
357
358 foreach ($tags as $tag) {
359 $taggedEntries[$tag] = $this->findIdentifiersByTag($tag);
360 }
361
362 $intersectedTaggedEntries = call_user_func_array('array_intersect', $taggedEntries);
363
364 foreach ($intersectedTaggedEntries as $entryIdentifier) {
365 $foundEntries[$entryIdentifier] = $entryIdentifier;
366 }
367
368 return $foundEntries;
369 }
370
371 /**
372 * Removes all cache entries of this cache.
373 *
374 * @return void
375 * @author Karsten Dambekalns <karsten@typo3.org>
376 */
377 public function flush() {
378 if (!$this->cache instanceof t3lib_cache_frontend_Frontend) {
379 throw new t3lib_cache_Exception('No cache frontend has been set via setCache() yet.', 1204111376);
380 }
381
382 $this->flushByTag('%MEMCACHEBE%' . $this->cache->getIdentifier());
383 }
384
385 /**
386 * Removes all cache entries of this cache which are tagged by the specified tag.
387 *
388 * @param string $tag The tag the entries must have
389 * @return void
390 * @author Karsten Dambekalns <karsten@typo3.org>
391 */
392 public function flushByTag($tag) {
393 $identifiers = $this->findIdentifiersByTag($tag);
394
395 foreach($identifiers as $identifier) {
396 $this->remove($identifier);
397 }
398 }
399
400
401 /**
402 * Removes all cache entries of this cache which are tagged by the specified tag.
403 *
404 * @param array The tags the entries must have
405 * @return void
406 * @author Ingo Renner <ingo@typo3.org>
407 */
408 public function flushByTags(array $tags) {
409 foreach ($tags as $tag) {
410 $this->flushByTag($tag);
411 }
412 }
413
414 /**
415 * Returns an array with all known tags
416 *
417 * @return array
418 * @author Karsten Dambekalns <karsten@typo3.org>
419 * @internal
420 */
421 protected function getTagIndex() {
422 $tagIndex = $this->memcache->get($this->identifierPrefix . 'tagIndex');
423 return ($tagIndex == false ? array() : (array)$tagIndex);
424 }
425
426 /**
427 * Saves the tags known to the backend
428 *
429 * @param array Array of tags
430 * @author Karsten Dambekalns <karsten@typo3.org>
431 * @internal
432 */
433 protected function setTagIndex(array $tags) {
434 $this->memcache->set($this->identifierPrefix . 'tagIndex', array_unique($tags), 0, 0);
435 }
436
437 /**
438 * Adds the given tags to the tag index
439 *
440 * @param array Array of tags
441 * @return void
442 * @author Karsten Dambekalns <karsten@typo3.org>
443 * @internal
444 */
445 protected function addTagsToTagIndex(array $tags) {
446 if(count($tags)) {
447 $this->setTagIndex(array_merge($tags, $this->getTagIndex()));
448 }
449 }
450
451 /**
452 * Removes the given tags from the tag index
453 *
454 * @param array $tags
455 * @return void
456 * @author Karsten Dambekalns <karsten@typo3.org>
457 * @internal
458 */
459 protected function removeTagsFromTagIndex(array $tags) {
460 if(count($tags)) {
461 $this->setTagIndex(array_diff($this->getTagIndex(), $tags));
462 }
463 }
464
465 /**
466 * Associates the identifier with the given tags
467 *
468 * @param string $entryIdentifier
469 * @param array Array of tags
470 * @author Karsten Dambekalns <karsten@typo3.org>
471 * @author Dmitry Dulepov <dmitry@typo3.org>
472 * @internal
473 */
474 protected function addIdentifierToTags($entryIdentifier, array $tags) {
475 if ($this->serverConnected) {
476 foreach($tags as $tag) {
477 // Update tag-to-identifier index
478 $identifiers = $this->findIdentifiersByTag($tag);
479 if (array_search($entryIdentifier, $identifiers) === false) {
480 $identifiers[] = $entryIdentifier;
481 $this->memcache->set($this->identifierPrefix . 'tag_' . $tag,
482 $identifiers);
483 }
484
485 // Update identifier-to-tag index
486 $existingTags = $this->findTagsByIdentifier($entryIdentifier);
487 if (array_search($entryIdentifier, $existingTags) === false) {
488 $this->memcache->set($this->identifierPrefix . 'ident_' . $entryIdentifier,
489 array_merge($existingTags, $tags));
490 }
491 }
492 }
493 }
494
495 /**
496 * Removes association of the identifier with the given tags
497 *
498 * @param string $entryIdentifier
499 * @param array Array of tags
500 * @author Karsten Dambekalns <karsten@typo3.org>
501 * @author Dmitry Dulepov <dmitry@typo3.org>
502 * @internal
503 */
504 protected function removeIdentifierFromAllTags($entryIdentifier) {
505 if ($this->serverConnected) {
506 // Get tags for this identifier
507 $tags = $this->findTagsByIdentifier($entryIdentifier);
508 // Deassociate tags with this identifier
509 foreach ($tags as $tag) {
510 $identifiers = $this->findIdentifiersByTag($tag);
511 // Formally array_search() below should never return false
512 // due to the behavior of findTagsForIdentifier(). But if
513 // reverse index is corrupted, we still can get 'false' from
514 // array_search(). This is not a problem because we are
515 // removing this identifier from anywhere.
516 if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
517 unset($identifiers[$key]);
518
519 if(count($identifiers)) {
520 $this->memcache->set(
521 $this->identifierPrefix . 'tag_' . $tag,
522 $identifiers
523 );
524 } else {
525 $this->removeTagsFromTagIndex(array($tag));
526 $this->memcache->delete($this->identifierPrefix . 'tag_' . $tag);
527 }
528 }
529 }
530
531 // Clear reverse tag index for this identifier
532 $this->memcache->delete($this->identifierPrefix . 'ident_' . $entryIdentifier);
533 }
534 }
535
536 /**
537 * Finds all tags for the given identifier. This function uses reverse tag
538 * index to search for tags.
539 *
540 * @param string Identifier to find tags by
541 * @return array Array with tags
542 * @author Dmitry Dulepov <dmitry@typo3.org>
543 * @internal
544 */
545 protected function findTagsByIdentifier($identifier) {
546 $tags = $this->memcache->get($this->identifierPrefix . 'ident_' . $identifier);
547 return ($tags === FALSE ? array() : (array)$tags);
548 }
549
550 /**
551 * Returns idenfier prefix. Extensions can override this function to provide
552 * another identifier prefix if it is necessary for special purposes.
553 * Default identifier prefix is based on PATH_site only. In most cases
554 * it is enough because different installations use different paths and page
555 * IDs in the same installation never repeat.
556 *
557 * @return string Identifier prefix, ending with underscore
558 * @author Dmitry Dulepov
559 */
560 protected function getIdentifierPrefix() {
561 return 'TYPO3_' . md5(PATH_site) . '_';
562 }
563
564 /**
565 * Does nothing, as memcached does GC itself
566 *
567 * @return void
568 */
569 public function collectGarbage() {
570 }
571 }
572
573
574 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']) {
575 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['t3lib/cache/backend/class.t3lib_cache_backend_memcachedbackend.php']);
576 }
577
578 ?>