Overview

Packages

  • Memcache

Classes

  • Horde_Memcache
  • Horde_Memcache_Exception
  • Overview
  • Package
  • Class
  • Tree
  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: 
API documentation generated by ApiGen