Files
server/usr/share/psa-pear/pear/php/Horde/Memcache.php
2026-01-07 20:52:11 +01:00

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();
}
}