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