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