1: <?php
2: /**
3: * This class provides an API or Horde code to interact with a centrally
4: * configured memcache installation.
5: *
6: * memcached website: http://www.danga.com/memcached/
7: *
8: * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
9: *
10: * See the enclosed file COPYING for license information (LGPL). If you
11: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
12: *
13: * @author Michael Slusarz <slusarz@horde.org>
14: * @author Didi Rieder <adrieder@sbox.tugraz.at>
15: * @category Horde
16: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
17: * @package Memcache
18: */
19: class Horde_Memcache implements Serializable
20: {
21: /**
22: * The number of bits reserved by PHP's memcache layer for internal flag
23: * use.
24: */
25: const FLAGS_RESERVED = 16;
26:
27: /**
28: * Locking timeout.
29: *
30: * @since 1.1.1
31: */
32: const LOCK_TIMEOUT = 30;
33:
34: /**
35: * Suffix added to key to create the lock entry.
36: *
37: * @since 1.1.0
38: */
39: const LOCK_SUFFIX = '_l';
40:
41: /**
42: * The max storage size of the memcache server. This should be slightly
43: * smaller than the actual value due to overhead. By default, the max
44: * slab size of memcached (as of 1.1.2) is 1 MB.
45: */
46: const MAX_SIZE = 1000000;
47:
48: /**
49: * Serializable version.
50: */
51: const VERSION = 1;
52:
53: /**
54: * Locked keys.
55: *
56: * @var array
57: */
58: protected $_locks = array();
59:
60: /**
61: * Memcache object.
62: *
63: * @var Memcache
64: */
65: protected $_memcache;
66:
67: /**
68: * Memcache defaults.
69: *
70: * @var array
71: */
72: protected $_params = array(
73: 'compression' => false,
74: 'hostspec' => array('localhost'),
75: 'large_items' => true,
76: 'persistent' => false,
77: 'port' => array(11211),
78: 'prefix' => 'horde'
79: );
80:
81: /**
82: * A list of items known not to exist.
83: *
84: * @var array
85: */
86: protected $_noexist = array();
87:
88: /**
89: * Logger instance.
90: *
91: * @var Horde_Log_Logger
92: */
93: protected $_logger;
94:
95: /**
96: * Constructor.
97: *
98: * @param array $params Configuration parameters:
99: * - compression: (boolean) Compress data inside memcache?
100: * DEFAULT: false
101: * - c_threshold: (integer) The minimum value length before attempting
102: * to compress.
103: * DEFAULT: none
104: * - hostspec: (array) The memcached host(s) to connect to.
105: * DEFAULT: 'localhost'
106: * - large_items: (boolean) Allow storing large data items (larger than
107: * Horde_Memcache::MAX_SIZE)?
108: * DEFAULT: true
109: * - persistent: (boolean) Use persistent DB connections?
110: * DEFAULT: false
111: * - prefix: (string) The prefix to use for the memcache keys.
112: * DEFAULT: 'horde'
113: * - port: (array) The port(s) memcache is listening on. Leave empty
114: * if using UNIX sockets.
115: * DEFAULT: 11211
116: * - weight: (array) The weight(s) to use for each memcached host.
117: * DEFAULT: none (equal weight to all servers)
118: *
119: * @throws Horde_Memcache_Exception
120: */
121: public function __construct(array $params = array())
122: {
123: $this->_params = array_merge($this->_params, $params);
124:
125: if (isset($params['logger'])) {
126: $this->_logger = $params['logger'];
127: }
128:
129: $this->_init();
130:
131: register_shutdown_function(array($this, 'shutdown'));
132: }
133:
134: /**
135: * Do initialization.
136: *
137: * @throws Horde_Memcache_Exception
138: */
139: public function _init()
140: {
141: $this->_memcache = new Memcache();
142:
143: $servers = array();
144: for ($i = 0, $n = count($this->_params['hostspec']); $i < $n; ++$i) {
145: if ($this->_memcache->addServer($this->_params['hostspec'][$i], empty($this->_params['port'][$i]) ? 0 : $this->_params['port'][$i], !empty($this->_params['persistent']), !empty($this->_params['weight'][$i]) ? $this->_params['weight'][$i] : 1)) {
146: $servers[] = $this->_params['hostspec'][$i] . (!empty($this->_params['port'][$i]) ? ':' . $this->_params['port'][$i] : '');
147: }
148: }
149:
150: /* Check if any of the connections worked. */
151: if (empty($servers)) {
152: throw new Horde_Memcache_Exception('Could not connect to any defined memcache servers.');
153: }
154:
155: if (!empty($this->_params['c_threshold'])) {
156: $this->_memcache->setCompressThreshold($this->_params['c_threshold']);
157: }
158:
159: // Force consistent hashing
160: ini_set('memcache.hash_strategy', 'consistent');
161:
162: if ($this->_logger) {
163: $this->_logger->log('Connected to the following memcache servers:' . implode($servers, ', '), 'DEBUG');
164: }
165: }
166:
167: /**
168: * Shutdown function.
169: *
170: * @since 1.1.0
171: */
172: public function shutdown()
173: {
174: foreach (array_keys($this->_locks) as $key) {
175: $this->unlock($key);
176: }
177: }
178:
179: /**
180: * Delete a key.
181: *
182: * @see Memcache::delete()
183: *
184: * @param string $key The key.
185: * @param integer $timeout Expiration time in seconds.
186: *
187: * @return boolean True on success.
188: */
189: public function delete($key, $timeout = 0)
190: {
191: return isset($this->_noexist[$key])
192: ? false
193: : $this->_memcache->delete($this->_key($key), $timeout);
194: }
195:
196: /**
197: * Get data associated with a key.
198: *
199: * @see Memcache::get()
200: *
201: * @param mixed $keys The key or an array of keys.
202: *
203: * @return mixed The string/array on success (return type is the type of
204: * $keys), false on failure.
205: */
206: public function get($keys)
207: {
208: $flags = null;
209: $key_map = $missing_parts = $os = $out_array = array();
210: $ret_array = true;
211:
212: if (!is_array($keys)) {
213: $keys = array($keys);
214: $ret_array = false;
215: }
216: $search_keys = $keys;
217:
218: foreach ($search_keys as $v) {
219: $key_map[$v] = $this->_key($v);
220: }
221:
222: if (($res = $this->_memcache->get(array_values($key_map), $flags)) === false) {
223: return false;
224: }
225:
226: /* Check to see if we have any oversize items we need to get. */
227: if (!empty($this->_params['large_items'])) {
228: foreach ($key_map as $key => $val) {
229: $part_count = isset($flags[$val])
230: ? ($flags[$val] >> self::FLAGS_RESERVED) - 1
231: : -1;
232:
233: switch ($part_count) {
234: case -1:
235: /* Ignore. */
236: unset($res[$val]);
237: break;
238:
239: case 0:
240: /* Not an oversize part. */
241: break;
242:
243: default:
244: $os[$key] = $this->_getOSKeyArray($key, $part_count);
245: foreach ($os[$key] as $val2) {
246: $missing_parts[] = $key_map[$val2] = $this->_key[$val2];
247: }
248: break;
249: }
250: }
251:
252: if (!empty($missing_parts)) {
253: if (($res2 = $this->_memcache->get($missing_parts)) === false) {
254: return false;
255: }
256:
257: /* $res should now contain the same results as if we had
258: * run a single get request with all keys above. */
259: $res = array_merge($res, $res2);
260: }
261: }
262:
263: foreach ($key_map as $k => $v) {
264: if (!isset($res[$v])) {
265: $this->_noexist[$k] = true;
266: }
267: }
268:
269: foreach ($keys as $k) {
270: $out_array[$k] = false;
271: if (isset($res[$key_map[$k]])) {
272: $data = $res[$key_map[$k]];
273: if (isset($os[$k])) {
274: foreach ($os[$k] as $v) {
275: if (isset($res[$key_map[$v]])) {
276: $data .= $res[$key_map[$v]];
277: } else {
278: $this->delete($k);
279: continue 2;
280: }
281: }
282: }
283: $out_array[$k] = @unserialize($data);
284: } elseif (isset($os[$k]) && !isset($res[$key_map[$k]])) {
285: $this->delete($k);
286: }
287: }
288:
289: return $ret_array
290: ? $out_array
291: : reset($out_array);
292: }
293:
294: /**
295: * Set the value of a key.
296: *
297: * @see Memcache::set()
298: *
299: * @param string $key The key.
300: * @param string $var The data to store.
301: * @param integer $timeout Expiration time in seconds.
302: *
303: * @return boolean True on success.
304: */
305: public function set($key, $var, $expire = 0)
306: {
307: return $this->_set($key, @serialize($var), $expire);
308: }
309:
310: /**
311: * Set the value of a key.
312: *
313: * @param string $key The key.
314: * @param string $var The data to store (serialized).
315: * @param integer $timeout Expiration time in seconds.
316: * @param integer $lent String length of $len.
317: *
318: * @return boolean True on success.
319: */
320: protected function _set($key, $var, $expire = 0, $len = null)
321: {
322: if (is_null($len)) {
323: $len = strlen($var);
324: }
325:
326: if (empty($this->_params['large_items']) && ($len > self::MAX_SIZE)) {
327: return false;
328: }
329:
330: for ($i = 0; ($i * self::MAX_SIZE) < $len; ++$i) {
331: $curr_key = $i ? ($key . '_s' . $i) : $key;
332:
333: $flags = $this->_getFlags($i ? 0 : ceil($len / self::MAX_SIZE));
334: $res = $this->_memcache->set($this->_key($curr_key), substr($var, $i * self::MAX_SIZE, self::MAX_SIZE), $flags, $expire);
335: if ($res === false) {
336: $this->delete($key);
337: break;
338: }
339: unset($this->_noexist[$curr_key]);
340: }
341:
342: return $res;
343: }
344:
345: /**
346: * Replace the value of a key.
347: *
348: * @see Memcache::replace()
349: *
350: * @param string $key The key.
351: * @param string $var The data to store.
352: * @param integer $timeout Expiration time in seconds.
353: *
354: * @return boolean True on success, false if key doesn't exist.
355: */
356: public function replace($key, $var, $expire = 0)
357: {
358: $var = @serialize($var);
359: $len = strlen($var);
360:
361: if ($len > self::MAX_SIZE) {
362: if (!empty($this->_params['large_items']) &&
363: $this->_memcache->get($this->_key($key))) {
364: return $this->_set($key, $var, $expire, $len);
365: }
366: return false;
367: }
368:
369: return $this->_memcache->replace($this->_key($key), $var, $this->_getFlags(1), $expire);
370: }
371:
372: /**
373: * Obtain lock on a key.
374: *
375: * @param string $key The key to lock.
376: */
377: public function lock($key)
378: {
379: while ($this->_memcache->add($this->_key($key . self::LOCK_SUFFIX), 1, 0, self::LOCK_TIMEOUT) === false) {
380: /* Wait 0.1 secs before attempting again. */
381: usleep(100000);
382: }
383:
384: $this->_locks[$key] = true;
385: }
386:
387: /**
388: * Release lock on a key.
389: *
390: * @param string $key The key to lock.
391: */
392: public function unlock($key)
393: {
394: $this->_memcache->delete($this->_key($key . self::LOCK_SUFFIX), 0);
395: unset($this->_locks[$key]);
396: }
397:
398: /**
399: * Mark all entries on a memcache installation as expired.
400: */
401: public function flush()
402: {
403: $this->_memcache->flush();
404: }
405:
406: /**
407: * Get the statistics output from the current memcache pool.
408: *
409: * @return array The output from Memcache::getExtendedStats() using the
410: * current configuration values.
411: */
412: public function stats()
413: {
414: return $this->_memcache->getExtendedStats();
415: }
416:
417: /**
418: * Obtains the md5 sum for a key.
419: *
420: * @param string $key The key.
421: *
422: * @return string The corresponding memcache key.
423: */
424: protected function _key($key)
425: {
426: return hash('md5', $this->_params['prefix'] . $key);
427: }
428:
429: /**
430: * Returns the key listing of all key IDs for an oversized item.
431: *
432: * @return array The array of key IDs.
433: */
434: protected function _getOSKeyArray($key, $length)
435: {
436: $ret = array();
437: for ($i = 0; $i < $length; ++$i) {
438: $ret[] = $key . '_s' . ($i + 1);
439: }
440: return $ret;
441: }
442:
443: /**
444: * Get flags for memcache call.
445: *
446: * @param integer $count
447: *
448: * @return integer
449: */
450: protected function _getFlags($count)
451: {
452: $flags = empty($this->_params['compression'])
453: ? 0
454: : MEMCACHE_COMPRESSED;
455: return ($flags | $count << self::FLAGS_RESERVED);
456: }
457:
458: /* Serializable methods. */
459:
460: /**
461: * Serialize.
462: *
463: * @return string Serialized representation of this object.
464: */
465: public function serialize()
466: {
467: return serialize(array(
468: self::VERSION,
469: $this->_params,
470: $this->_logger
471: ));
472: }
473:
474: /**
475: * Unserialize.
476: *
477: * @param string $data Serialized data.
478: *
479: * @throws Exception
480: * @throws Horde_Memcache_Exception
481: */
482: public function unserialize($data)
483: {
484: $data = @unserialize($data);
485: if (!is_array($data) ||
486: !isset($data[0]) ||
487: ($data[0] != self::VERSION)) {
488: throw new Exception('Cache version change');
489: }
490:
491: $this->_params = $data[1];
492: $this->_logger = $data[2];
493:
494: $this->_init();
495: }
496:
497: }
498: