runff 1.0 commit
This commit is contained in:
492
lib/SimpleSAML/Memcache.php
Executable file
492
lib/SimpleSAML/Memcache.php
Executable file
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
|
||||
/**
|
||||
* This file implements functions to read and write to a group of memcache
|
||||
* servers.
|
||||
*
|
||||
* The goals of this storage class is to provide failover, redudancy and load
|
||||
* balancing. This is accomplished by storing the data object to several
|
||||
* groups of memcache servers. Each data object is replicated to every group
|
||||
* of memcache servers, but it is only stored to one server in each group.
|
||||
*
|
||||
* For this code to work correctly, all web servers accessing the data must
|
||||
* have the same clock (as measured by the time()-function). Different clock
|
||||
* values will lead to incorrect behaviour.
|
||||
*
|
||||
* @author Olav Morken, UNINETT AS.
|
||||
* @package SimpleSAMLphp
|
||||
*/
|
||||
class SimpleSAML_Memcache
|
||||
{
|
||||
|
||||
/**
|
||||
* Cache of the memcache servers we are using.
|
||||
*
|
||||
* @var Memcache[]|null
|
||||
*/
|
||||
private static $serverGroups = null;
|
||||
|
||||
|
||||
/**
|
||||
* The flavor of memcache PHP extension we are using.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $extension = '';
|
||||
|
||||
|
||||
/**
|
||||
* Find data stored with a given key.
|
||||
*
|
||||
* @param string $key The key of the data.
|
||||
*
|
||||
* @return mixed The data stored with the given key, or null if no data matching the key was found.
|
||||
*/
|
||||
public static function get($key)
|
||||
{
|
||||
SimpleSAML\Logger::debug("loading key $key from memcache");
|
||||
|
||||
$latestInfo = null;
|
||||
$latestTime = 0.0;
|
||||
$latestData = null;
|
||||
$mustUpdate = false;
|
||||
$allDown = true;
|
||||
|
||||
// search all the servers for the given id
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
$serializedInfo = $server->get($key);
|
||||
if ($serializedInfo === false) {
|
||||
// either the server is down, or we don't have the value stored on that server
|
||||
$mustUpdate = true;
|
||||
$up = $server->getstats();
|
||||
if ($up !== false) {
|
||||
$allDown = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$allDown = false;
|
||||
|
||||
// unserialize the object
|
||||
$info = unserialize($serializedInfo);
|
||||
|
||||
/*
|
||||
* Make sure that this is an array with two keys:
|
||||
* - 'timestamp': The time the data was saved.
|
||||
* - 'data': The data.
|
||||
*/
|
||||
if (!is_array($info)) {
|
||||
SimpleSAML\Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Data was not an array.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists('timestamp', $info)) {
|
||||
SimpleSAML\Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Missing timestamp.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists('data', $info)) {
|
||||
SimpleSAML\Logger::warning(
|
||||
'Retrieved invalid data from a memcache server. Missing data.'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($latestInfo === null) {
|
||||
// first info found
|
||||
$latestInfo = $serializedInfo;
|
||||
$latestTime = $info['timestamp'];
|
||||
$latestData = $info['data'];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($info['timestamp'] === $latestTime && $serializedInfo === $latestInfo) {
|
||||
// this data matches the data from the other server(s)
|
||||
continue;
|
||||
}
|
||||
|
||||
// different data from different servers. We need to update at least one of them to maintain sync
|
||||
$mustUpdate = true;
|
||||
|
||||
// update if data in $info is newer than $latestData
|
||||
if ($latestTime < $info['timestamp']) {
|
||||
$latestInfo = $serializedInfo;
|
||||
$latestTime = $info['timestamp'];
|
||||
$latestData = $info['data'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($latestData === null) {
|
||||
if ($allDown) {
|
||||
// all servers are down, panic!
|
||||
$e = new SimpleSAML_Error_Error('MEMCACHEDOWN', null, 503);
|
||||
throw new SimpleSAML_Error_Exception('All memcache servers are down', 503, $e);
|
||||
}
|
||||
// we didn't find any data matching the key
|
||||
SimpleSAML\Logger::debug("key $key not found in memcache");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($mustUpdate) {
|
||||
// we found data matching the key, but some of the servers need updating
|
||||
SimpleSAML\Logger::debug("Memcache servers out of sync for $key, forcing sync");
|
||||
self::set($key, $latestData);
|
||||
}
|
||||
|
||||
return $latestData;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save a key-value pair to the memcache servers.
|
||||
*
|
||||
* @param string $key The key of the data.
|
||||
* @param mixed $value The value of the data.
|
||||
* @param integer|null $expire The expiration timestamp of the data.
|
||||
*/
|
||||
public static function set($key, $value, $expire = null)
|
||||
{
|
||||
SimpleSAML\Logger::debug("saving key $key to memcache");
|
||||
$savedInfo = array(
|
||||
'timestamp' => microtime(true),
|
||||
'data' => $value
|
||||
);
|
||||
|
||||
if ($expire === null) {
|
||||
$expire = self::getExpireTime();
|
||||
}
|
||||
|
||||
$savedInfoSerialized = serialize($savedInfo);
|
||||
|
||||
// store this object to all groups of memcache servers
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
if (self::$extension === 'memcached') {
|
||||
$server->set($key, $savedInfoSerialized, $expire);
|
||||
} else {
|
||||
$server->set($key, $savedInfoSerialized, 0, $expire);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a key-value pair from the memcache servers.
|
||||
*
|
||||
* @param string $key The key we should delete.
|
||||
*/
|
||||
public static function delete($key)
|
||||
{
|
||||
assert(is_string($key));
|
||||
SimpleSAML\Logger::debug("deleting key $key from memcache");
|
||||
|
||||
// store this object to all groups of memcache servers
|
||||
foreach (self::getMemcacheServers() as $server) {
|
||||
$server->delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function adds a server from the 'memcache_store.servers'
|
||||
* configuration option to a Memcache object.
|
||||
*
|
||||
* The server parameter is an array with the following keys:
|
||||
* - hostname
|
||||
* Hostname or ip address to the memcache server.
|
||||
* - port (optional)
|
||||
* port number the memcache server is running on. This
|
||||
* defaults to memcache.default_port if no value is given.
|
||||
* The default value of memcache.default_port is 11211.
|
||||
* - weight (optional)
|
||||
* The weight of this server in the load balancing
|
||||
* cluster.
|
||||
* - timeout (optional)
|
||||
* The timeout for contacting this server, in seconds.
|
||||
* The default value is 3 seconds.
|
||||
*
|
||||
* @param Memcache $memcache The Memcache object we should add this server to.
|
||||
* @param array $server An associative array with the configuration options for the server to add.
|
||||
*
|
||||
* @throws Exception If any configuration option for the server is invalid.
|
||||
*/
|
||||
private static function addMemcacheServer($memcache, $server)
|
||||
{
|
||||
// the hostname option is required
|
||||
if (!array_key_exists('hostname', $server)) {
|
||||
throw new Exception(
|
||||
"hostname setting missing from server in the 'memcache_store.servers' configuration option."
|
||||
);
|
||||
}
|
||||
|
||||
$hostname = $server['hostname'];
|
||||
|
||||
// the hostname must be a valid string
|
||||
if (!is_string($hostname)) {
|
||||
throw new Exception(
|
||||
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is".
|
||||
' supposed to be a string.'
|
||||
);
|
||||
}
|
||||
|
||||
// check if we are told to use a socket
|
||||
$socket = false;
|
||||
if (strpos($hostname, 'unix:///') === 0) {
|
||||
$socket = true;
|
||||
}
|
||||
|
||||
// check if the user has specified a port number
|
||||
if ($socket) {
|
||||
// force port to be 0 for sockets
|
||||
$port = 0;
|
||||
} elseif (array_key_exists('port', $server)) {
|
||||
// get the port number from the array, and validate it
|
||||
$port = (int) $server['port'];
|
||||
if (($port <= 0) || ($port > 65535)) {
|
||||
throw new Exception(
|
||||
"Invalid port for server in the 'memcache_store.servers' configuration option. The port number".
|
||||
' is supposed to be an integer between 0 and 65535.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use the default port number from the ini-file
|
||||
$port = (int) ini_get('memcache.default_port');
|
||||
if ($port <= 0 || $port > 65535) {
|
||||
// invalid port number from the ini-file. fall back to the default
|
||||
$port = 11211;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the user has specified a weight for this server
|
||||
if (array_key_exists('weight', $server)) {
|
||||
// get the weight and validate it
|
||||
$weight = (int) $server['weight'];
|
||||
if ($weight <= 0) {
|
||||
throw new Exception(
|
||||
"Invalid weight for server in the 'memcache_store.servers' configuration option. The weight is".
|
||||
' supposed to be a positive integer.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use a default weight of 1
|
||||
$weight = 1;
|
||||
}
|
||||
|
||||
// check if the user has specified a timeout for this server
|
||||
if (array_key_exists('timeout', $server)) {
|
||||
// get the timeout and validate it
|
||||
$timeout = (int) $server['timeout'];
|
||||
if ($timeout <= 0) {
|
||||
throw new Exception(
|
||||
"Invalid timeout for server in the 'memcache_store.servers' configuration option. The timeout is".
|
||||
' supposed to be a positive integer.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// use a default timeout of 3 seconds
|
||||
$timeout = 3;
|
||||
}
|
||||
|
||||
// add this server to the Memcache object
|
||||
if (self::$extension === 'memcached') {
|
||||
$memcache->addServer($hostname, $port);
|
||||
} else {
|
||||
$memcache->addServer($hostname, $port, true, $weight, $timeout, $timeout, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function takes in a list of servers belonging to a group and
|
||||
* creates a Memcache object from the servers in the group.
|
||||
*
|
||||
* @param array $group Array of servers which should be created as a group.
|
||||
*
|
||||
* @return Memcache A Memcache object of the servers in the group
|
||||
*
|
||||
* @throws Exception If the servers configuration is invalid.
|
||||
*/
|
||||
private static function loadMemcacheServerGroup(array $group)
|
||||
{
|
||||
$class = class_exists('Memcache') ? 'Memcache' : (class_exists('Memcached') ? 'Memcached' : false);
|
||||
if (!$class) {
|
||||
throw new Exception('Missing Memcached implementation. You must install either the Memcache or Memcached extension.');
|
||||
}
|
||||
self::$extension = strtolower($class);
|
||||
|
||||
// create the Memcache object
|
||||
$memcache = new $class();
|
||||
|
||||
// iterate over all the servers in the group and add them to the Memcache object
|
||||
foreach ($group as $index => $server) {
|
||||
// make sure that we don't have an index. An index would be a sign of invalid configuration
|
||||
if (!is_int($index)) {
|
||||
throw new Exception(
|
||||
"Invalid index on element in the 'memcache_store.servers' configuration option. Perhaps you".
|
||||
' have forgotten to add an array(...) around one of the server groups? The invalid index was: '.
|
||||
$index
|
||||
);
|
||||
}
|
||||
|
||||
// make sure that the server object is an array. Each server is an array with name-value pairs
|
||||
if (!is_array($server)) {
|
||||
throw new Exception(
|
||||
'Invalid value for the server with index '.$index.
|
||||
'. Remeber that the \'memcache_store.servers\' configuration option'.
|
||||
' contains an array of arrays of arrays.'
|
||||
);
|
||||
}
|
||||
|
||||
self::addMemcacheServer($memcache, $server);
|
||||
}
|
||||
|
||||
return $memcache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function gets a list of all configured memcache servers. This list is initialized based
|
||||
* on the content of 'memcache_store.servers' in the configuration.
|
||||
*
|
||||
* @return Memcache[] Array with Memcache objects.
|
||||
*
|
||||
* @throws Exception If the servers configuration is invalid.
|
||||
*/
|
||||
private static function getMemcacheServers()
|
||||
{
|
||||
// check if we have loaded the servers already
|
||||
if (self::$serverGroups != null) {
|
||||
return self::$serverGroups;
|
||||
}
|
||||
|
||||
// initialize the servers-array
|
||||
self::$serverGroups = array();
|
||||
|
||||
// load the configuration
|
||||
$config = SimpleSAML_Configuration::getInstance();
|
||||
|
||||
|
||||
$groups = $config->getArray('memcache_store.servers');
|
||||
|
||||
// iterate over all the groups in the 'memcache_store.servers' configuration option
|
||||
foreach ($groups as $index => $group) {
|
||||
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
|
||||
if (!is_int($index)) {
|
||||
throw new Exception(
|
||||
"Invalid index on element in the 'memcache_store.servers'".
|
||||
' configuration option. Perhaps you have forgotten to add an array(...)'.
|
||||
' around one of the server groups? The invalid index was: '.$index
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Make sure that the group is an array. Each group is an array of servers. Each server is
|
||||
* an array of name => value pairs for that server.
|
||||
*/
|
||||
if (!is_array($group)) {
|
||||
throw new Exception(
|
||||
"Invalid value for the server with index ".$index.
|
||||
". Remeber that the 'memcache_store.servers' configuration option".
|
||||
' contains an array of arrays of arrays.'
|
||||
);
|
||||
}
|
||||
|
||||
// parse and add this group to the server group list
|
||||
self::$serverGroups[] = self::loadMemcacheServerGroup($group);
|
||||
}
|
||||
|
||||
return self::$serverGroups;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a helper-function which returns the expire value of data
|
||||
* we should store to the memcache servers.
|
||||
*
|
||||
* The value is set depending on the configuration. If no value is
|
||||
* set in the configuration, then we will use a default value of 0.
|
||||
* 0 means that the item will never expire.
|
||||
*
|
||||
* @return integer The value which should be passed in the set(...) calls to the memcache objects.
|
||||
*
|
||||
* @throws Exception If the option 'memcache_store.expires' has a negative value.
|
||||
*/
|
||||
private static function getExpireTime()
|
||||
{
|
||||
// get the configuration instance
|
||||
$config = SimpleSAML_Configuration::getInstance();
|
||||
assert($config instanceof SimpleSAML_Configuration);
|
||||
|
||||
// get the expire-value from the configuration
|
||||
$expire = $config->getInteger('memcache_store.expires', 0);
|
||||
|
||||
// it must be a positive integer
|
||||
if ($expire < 0) {
|
||||
throw new Exception(
|
||||
"The value of 'memcache_store.expires' in the configuration can't be a negative integer."
|
||||
);
|
||||
}
|
||||
|
||||
/* If the configuration option is 0, then we should return 0. This allows the user to specify that the data
|
||||
* shouldn't expire.
|
||||
*/
|
||||
if ($expire == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The expire option is given as the number of seconds into the future an item should expire. We convert this
|
||||
* to an actual timestamp.
|
||||
*/
|
||||
$expireTime = time() + $expire;
|
||||
|
||||
return $expireTime;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function retrieves statistics about all memcache server groups.
|
||||
*
|
||||
* @return array Array with the names of each stat and an array with the value for each server group.
|
||||
*
|
||||
* @throws Exception If memcache server status couldn't be retrieved.
|
||||
*/
|
||||
public static function getStats()
|
||||
{
|
||||
$ret = array();
|
||||
|
||||
foreach (self::getMemcacheServers() as $sg) {
|
||||
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
|
||||
foreach ($stats as $server => $data) {
|
||||
if ($data === false) {
|
||||
throw new Exception('Failed to get memcache server status.');
|
||||
}
|
||||
}
|
||||
|
||||
$stats = SimpleSAML\Utils\Arrays::transpose($stats);
|
||||
|
||||
$ret = array_merge_recursive($ret, $stats);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve statistics directly in the form returned by getExtendedStats, for
|
||||
* all server groups.
|
||||
*
|
||||
* @return array An array with the extended stats output for each server group.
|
||||
*/
|
||||
public static function getRawStats()
|
||||
{
|
||||
$ret = array();
|
||||
|
||||
foreach (self::getMemcacheServers() as $sg) {
|
||||
$stats = method_exists($sg, 'getExtendedStats') ? $sg->getExtendedStats() : $sg->getStats();
|
||||
$ret[] = $stats;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user