612 lines
18 KiB
PHP
612 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2007-2016 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @author Didi Rieder <adrieder@sbox.tugraz.at>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Memcache
|
|
*/
|
|
|
|
/**
|
|
* This class provides an API or Horde code to interact with a centrally
|
|
* configured memcache installation.
|
|
*
|
|
* memcached website: http://www.danga.com/memcached/
|
|
*
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @author Didi Rieder <adrieder@sbox.tugraz.at>
|
|
* @category Horde
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Memcache
|
|
*/
|
|
class Horde_Memcache implements Serializable
|
|
{
|
|
/**
|
|
* The number of bits reserved by PHP's memcache layer for internal flag
|
|
* use.
|
|
*/
|
|
const FLAGS_RESERVED = 16;
|
|
|
|
/**
|
|
* Locking timeout.
|
|
*/
|
|
const LOCK_TIMEOUT = 30;
|
|
|
|
/**
|
|
* Suffix added to key to create the lock entry.
|
|
*/
|
|
const LOCK_SUFFIX = '_l';
|
|
|
|
/**
|
|
* The max storage size of the memcache server. This should be slightly
|
|
* smaller than the actual value due to overhead. By default, the max
|
|
* slab size of memcached (as of 1.1.2) is 1 MB.
|
|
*/
|
|
const MAX_SIZE = 1000000;
|
|
|
|
/**
|
|
* Serializable version.
|
|
*/
|
|
const VERSION = 1;
|
|
|
|
/**
|
|
* Locked keys.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_locks = array();
|
|
|
|
/**
|
|
* Logger instance.
|
|
*
|
|
* @var Horde_Log_Logger
|
|
*/
|
|
protected $_logger;
|
|
|
|
/**
|
|
* Memcache object.
|
|
*
|
|
* @var Memcache
|
|
*/
|
|
protected $_memcache;
|
|
|
|
/**
|
|
* A list of items known not to exist.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_noexist = array();
|
|
|
|
/**
|
|
* Memcache defaults.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_params = array(
|
|
'compression' => false,
|
|
'hostspec' => array('localhost'),
|
|
'large_items' => true,
|
|
'persistent' => false,
|
|
'port' => array(11211),
|
|
'prefix' => 'horde'
|
|
);
|
|
|
|
/**
|
|
* The list of active servers.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_servers = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $params Configuration parameters:
|
|
* - compression: (boolean) Compress data inside memcache?
|
|
* DEFAULT: false
|
|
* - c_threshold: (integer) The minimum value length before attempting
|
|
* to compress.
|
|
* DEFAULT: none
|
|
* - hostspec: (array) The memcached host(s) to connect to.
|
|
* DEFAULT: 'localhost'
|
|
* - large_items: (boolean) Allow storing large data items (larger than
|
|
* Horde_Memcache::MAX_SIZE)? Currently not supported with
|
|
* memcached extension.
|
|
* DEFAULT: true
|
|
* - persistent: (boolean) Use persistent DB connections?
|
|
* DEFAULT: false
|
|
* - prefix: (string) The prefix to use for the memcache keys.
|
|
* DEFAULT: 'horde'
|
|
* - port: (array) The port(s) memcache is listening on. Leave empty
|
|
* if using UNIX sockets.
|
|
* DEFAULT: 11211
|
|
* - weight: (array) The weight(s) to use for each memcached host.
|
|
* DEFAULT: none (equal weight to all servers)
|
|
*
|
|
* @throws Horde_Memcache_Exception
|
|
*/
|
|
public function __construct(array $params = array())
|
|
{
|
|
$this->_params = array_merge($this->_params, $params);
|
|
$this->_init();
|
|
}
|
|
|
|
/**
|
|
* Do initialization.
|
|
*
|
|
* @throws Horde_Memcache_Exception
|
|
*/
|
|
public function _init()
|
|
{
|
|
if (class_exists('Memcached')) {
|
|
if (empty($this->_params['persistent'])) {
|
|
$this->_memcache = new Memcached();
|
|
} else {
|
|
$this->_memcache = new Memcached('horde_memcache');
|
|
}
|
|
$this->_params['large_items'] = false;
|
|
$this->_memcache->setOptions(array(
|
|
Memcached::OPT_COMPRESSION => $this->_params['compression'],
|
|
Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT,
|
|
Memcached::OPT_HASH => Memcached::HASH_MD5,
|
|
Memcached::OPT_LIBKETAMA_COMPATIBLE => true,
|
|
Memcached::OPT_PREFIX_KEY => $this->_params['prefix'],
|
|
));
|
|
} else {
|
|
// Force consistent hashing
|
|
ini_set('memcache.hash_strategy', 'consistent');
|
|
$this->_memcache = new Memcache();
|
|
}
|
|
|
|
for ($i = 0, $n = count($this->_params['hostspec']); $i < $n; ++$i) {
|
|
if ($this->_memcache instanceof Memcached) {
|
|
$res = $this->_memcache->addServer(
|
|
$this->_params['hostspec'][$i],
|
|
empty($this->_params['port'][$i]) ? 0 : $this->_params['port'][$i],
|
|
!empty($this->_params['weight'][$i]) ? $this->_params['weight'][$i] : 0
|
|
);
|
|
} else {
|
|
$res = $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,
|
|
1,
|
|
15,
|
|
true,
|
|
array($this, 'failover')
|
|
);
|
|
}
|
|
|
|
if ($res) {
|
|
$this->_servers[] = $this->_params['hostspec'][$i] . (!empty($this->_params['port'][$i]) ? ':' . $this->_params['port'][$i] : '');
|
|
}
|
|
}
|
|
|
|
/* Check if any of the connections worked. */
|
|
if (empty($this->_servers)) {
|
|
throw new Horde_Memcache_Exception('Could not connect to any defined memcache servers.');
|
|
}
|
|
|
|
if ($this->_memcache instanceof Memcache &&
|
|
!empty($this->_params['c_threshold'])) {
|
|
$this->_memcache->setCompressThreshold($this->_params['c_threshold']);
|
|
}
|
|
|
|
if (isset($this->_params['logger'])) {
|
|
$this->_logger = $this->_params['logger'];
|
|
$this->_logger->log('Connected to the following memcache servers:' . implode($this->_servers, ', '), 'DEBUG');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shutdown function.
|
|
*/
|
|
public function shutdown()
|
|
{
|
|
foreach (array_keys($this->_locks) as $key) {
|
|
$this->unlock($key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a key.
|
|
*
|
|
* @see Memcache::delete()
|
|
*
|
|
* @param string $key The key.
|
|
* @param integer $timeout Expiration time in seconds.
|
|
*
|
|
* @return boolean True on success.
|
|
*/
|
|
public function delete($key, $timeout = 0)
|
|
{
|
|
return isset($this->_noexist[$key])
|
|
? false
|
|
: $this->_memcache->delete($this->_key($key), $timeout);
|
|
}
|
|
|
|
/**
|
|
* Get data associated with a key.
|
|
*
|
|
* @see Memcache::get()
|
|
*
|
|
* @param mixed $keys The key or an array of keys.
|
|
*
|
|
* @return mixed The string/array on success (return type is the type of
|
|
* $keys), false on failure.
|
|
*/
|
|
public function get($keys)
|
|
{
|
|
$flags = null;
|
|
$key_map = $missing_parts = $os = $out_array = array();
|
|
$ret_array = true;
|
|
|
|
if (!is_array($keys)) {
|
|
$keys = array($keys);
|
|
$ret_array = false;
|
|
}
|
|
$search_keys = $keys;
|
|
|
|
foreach ($search_keys as $v) {
|
|
$key_map[$v] = $this->_key($v);
|
|
}
|
|
|
|
if ($this->_memcache instanceof Memcached) {
|
|
$res = $this->_memcache->getMulti(array_values($key_map));
|
|
} else {
|
|
$res = $this->_memcache->get(array_values($key_map), $flags);
|
|
}
|
|
if ($res === false) {
|
|
return false;
|
|
}
|
|
|
|
/* Check to see if we have any oversize items we need to get. */
|
|
if (!empty($this->_params['large_items'])) {
|
|
foreach ($key_map as $key => $val) {
|
|
$part_count = isset($flags[$val])
|
|
? ($flags[$val] >> self::FLAGS_RESERVED) - 1
|
|
: -1;
|
|
|
|
switch ($part_count) {
|
|
case -1:
|
|
/* Ignore. */
|
|
unset($res[$val]);
|
|
break;
|
|
|
|
case 0:
|
|
/* Not an oversize part. */
|
|
break;
|
|
|
|
default:
|
|
$os[$key] = $this->_getOSKeyArray($key, $part_count);
|
|
foreach ($os[$key] as $val2) {
|
|
$missing_parts[] = $key_map[$val2] = $this->_key($val2);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!empty($missing_parts)) {
|
|
if (($res2 = $this->_memcache->get($missing_parts)) === false) {
|
|
return false;
|
|
}
|
|
|
|
/* $res should now contain the same results as if we had
|
|
* run a single get request with all keys above. */
|
|
$res = array_merge($res, $res2);
|
|
}
|
|
}
|
|
|
|
foreach ($key_map as $k => $v) {
|
|
if (!isset($res[$v])) {
|
|
$this->_noexist[$k] = true;
|
|
}
|
|
}
|
|
|
|
foreach ($keys as $k) {
|
|
$out_array[$k] = false;
|
|
if (isset($res[$key_map[$k]])) {
|
|
$data = $res[$key_map[$k]];
|
|
if (isset($os[$k])) {
|
|
foreach ($os[$k] as $v) {
|
|
if (isset($res[$key_map[$v]])) {
|
|
$data .= $res[$key_map[$v]];
|
|
} else {
|
|
$this->delete($k);
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
$out_array[$k] = @unserialize($data);
|
|
} elseif (isset($os[$k]) && !isset($res[$key_map[$k]])) {
|
|
$this->delete($k);
|
|
}
|
|
}
|
|
|
|
return $ret_array
|
|
? $out_array
|
|
: reset($out_array);
|
|
}
|
|
|
|
/**
|
|
* Set the value of a key.
|
|
*
|
|
* @see Memcache::set()
|
|
*
|
|
* @param string $key The key.
|
|
* @param string $var The data to store.
|
|
* @param integer $timeout Expiration time in seconds.
|
|
*
|
|
* @return boolean True on success.
|
|
*/
|
|
public function set($key, $var, $expire = 0)
|
|
{
|
|
return $this->_set($key, @serialize($var), $expire);
|
|
}
|
|
|
|
/**
|
|
* Set the value of a key.
|
|
*
|
|
* @param string $key The key.
|
|
* @param string $var The data to store (serialized).
|
|
* @param integer $timeout Expiration time in seconds.
|
|
* @param integer $lent String length of $len.
|
|
*
|
|
* @return boolean True on success.
|
|
*/
|
|
protected function _set($key, $var, $expire = 0, $len = null)
|
|
{
|
|
if (is_null($len)) {
|
|
$len = strlen($var);
|
|
}
|
|
|
|
if (empty($this->_params['large_items']) && ($len > self::MAX_SIZE)) {
|
|
return false;
|
|
}
|
|
|
|
for ($i = 0; ($i * self::MAX_SIZE) < $len; ++$i) {
|
|
$curr_key = $i ? ($key . '_s' . $i) : $key;
|
|
$res = $this->_memcache instanceof Memcached
|
|
? $this->_memcache->set($curr_key, $var, $expire)
|
|
: $this->_memcache->set(
|
|
$this->_key($curr_key),
|
|
substr($var, $i * self::MAX_SIZE, self::MAX_SIZE),
|
|
$this->_getFlags($i ? 0 : ceil($len / self::MAX_SIZE)),
|
|
$expire
|
|
);
|
|
if ($res === false) {
|
|
$this->delete($key);
|
|
break;
|
|
}
|
|
unset($this->_noexist[$curr_key]);
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Replace the value of a key.
|
|
*
|
|
* @see Memcache::replace()
|
|
*
|
|
* @param string $key The key.
|
|
* @param string $var The data to store.
|
|
* @param integer $timeout Expiration time in seconds.
|
|
*
|
|
* @return boolean True on success, false if key doesn't exist.
|
|
*/
|
|
public function replace($key, $var, $expire = 0)
|
|
{
|
|
$var = @serialize($var);
|
|
$len = strlen($var);
|
|
|
|
if ($len > self::MAX_SIZE) {
|
|
if (!empty($this->_params['large_items']) &&
|
|
$this->_memcache->get($this->_key($key))) {
|
|
return $this->_set($key, $var, $expire, $len);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return $this->_memcache instanceof Memcached
|
|
? $this->_memcache->replace($key, $var, $expire)
|
|
: $this->_memcache->replace(
|
|
$this->_key($key), $var, $this->_getFlags(1), $expire
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Obtain lock on a key.
|
|
*
|
|
* @param string $key The key to lock.
|
|
*/
|
|
public function lock($key)
|
|
{
|
|
$i = 0;
|
|
|
|
while ($this->_lockAdd($key) === false) {
|
|
usleep(min(pow(2, $i++) * 10000, 100000));
|
|
}
|
|
|
|
/* Register a shutdown handler function here to catch cases where PHP
|
|
* suffers a fatal error. Must be done via shutdown function, since
|
|
* a destructor will not be called in this case.
|
|
* Only trigger on error, since we must assume that the code that
|
|
* locked will also handle unlocks (which may occur in the destruct
|
|
* phase, e.g. session handling).
|
|
* @todo: $this is not usable in closures until PHP 5.4+ */
|
|
if (empty($this->_locks)) {
|
|
$self = $this;
|
|
register_shutdown_function(function() use ($self) {
|
|
$e = error_get_last();
|
|
if ($e['type'] & E_ERROR) {
|
|
/* Try to do cleanup at very end of shutdown methods. */
|
|
register_shutdown_function(array($self, 'shutdown'));
|
|
}
|
|
});
|
|
}
|
|
|
|
$this->_locks[$key] = true;
|
|
}
|
|
|
|
/**
|
|
* Small wrapper around Memcache[d]#add().
|
|
*
|
|
* @param string $key The key to lock.
|
|
*/
|
|
protected function _lockAdd($key)
|
|
{
|
|
if ($this->_memcache instanceof Memcached) {
|
|
$this->_memcache->add(
|
|
$this->_key($key . self::LOCK_SUFFIX), 1, self::LOCK_TIMEOUT
|
|
);
|
|
} else {
|
|
$this->_memcache->add(
|
|
$this->_key($key . self::LOCK_SUFFIX), 1, 0, self::LOCK_TIMEOUT
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Release lock on a key.
|
|
*
|
|
* @param string $key The key to lock.
|
|
*/
|
|
public function unlock($key)
|
|
{
|
|
$this->_memcache->delete($this->_key($key . self::LOCK_SUFFIX), 0);
|
|
unset($this->_locks[$key]);
|
|
}
|
|
|
|
/**
|
|
* Mark all entries on a memcache installation as expired.
|
|
*/
|
|
public function flush()
|
|
{
|
|
$this->_memcache->flush();
|
|
}
|
|
|
|
/**
|
|
* Get the statistics output from the current memcache pool.
|
|
*
|
|
* @return array The output from Memcache::getExtendedStats() using the
|
|
* current configuration values.
|
|
*/
|
|
public function stats()
|
|
{
|
|
return $this->_memcache instanceof Memcached
|
|
? $this->_memcache->getStats()
|
|
: $this->_memcache->getExtendedStats();
|
|
}
|
|
|
|
/**
|
|
* Failover method.
|
|
*
|
|
* @see Memcache::addServer()
|
|
*
|
|
* @param string $host Hostname.
|
|
* @param integer $port Port.
|
|
*
|
|
* @throws Horde_Memcache_Exception
|
|
*/
|
|
public function failover($host, $port)
|
|
{
|
|
$pos = array_search($host . ':' . $port, $this->_servers);
|
|
if ($pos !== false) {
|
|
unset($this->_servers[$pos]);
|
|
if (!count($this->_servers)) {
|
|
throw new Horde_Memcache_Exception('Could not connect to any defined memcache servers.');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtains the md5 sum for a key.
|
|
*
|
|
* @param string $key The key.
|
|
*
|
|
* @return string The corresponding memcache key.
|
|
*/
|
|
protected function _key($key)
|
|
{
|
|
return $this->_memcache instanceof Memcached
|
|
? $key
|
|
: hash('md5', $this->_params['prefix'] . $key);
|
|
}
|
|
|
|
/**
|
|
* Returns the key listing of all key IDs for an oversized item.
|
|
*
|
|
* @return array The array of key IDs.
|
|
*/
|
|
protected function _getOSKeyArray($key, $length)
|
|
{
|
|
$ret = array();
|
|
for ($i = 0; $i < $length; ++$i) {
|
|
$ret[] = $key . '_s' . ($i + 1);
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Get flags for memcache call.
|
|
*
|
|
* @param integer $count
|
|
*
|
|
* @return integer
|
|
*/
|
|
protected function _getFlags($count)
|
|
{
|
|
$flags = empty($this->_params['compression'])
|
|
? 0
|
|
: MEMCACHE_COMPRESSED;
|
|
return ($flags | $count << self::FLAGS_RESERVED);
|
|
}
|
|
|
|
/* Serializable methods. */
|
|
|
|
/**
|
|
* Serialize.
|
|
*
|
|
* @return string Serialized representation of this object.
|
|
*/
|
|
public function serialize()
|
|
{
|
|
return serialize(array(
|
|
self::VERSION,
|
|
$this->_params
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Unserialize.
|
|
*
|
|
* @param string $data Serialized data.
|
|
*
|
|
* @throws Exception
|
|
* @throws Horde_Memcache_Exception
|
|
*/
|
|
public function unserialize($data)
|
|
{
|
|
$data = @unserialize($data);
|
|
if (!is_array($data) ||
|
|
!isset($data[0]) ||
|
|
($data[0] != self::VERSION)) {
|
|
throw new Exception('Cache version change');
|
|
}
|
|
|
|
$this->_params = $data[1];
|
|
|
|
$this->_init();
|
|
}
|
|
|
|
}
|