Newer
Older
namespace SimpleSAML;
use SimpleSAML\Error\ErrorCodes;
use SimpleSAML\Utils;
use function array_key_exists;
use function array_merge_recursive;
use function count;
use function ini_get;
use function is_array;
use function is_int;
use function is_string;
use function microtime;
use function serialize;
use function strpos;
use function time;
use function unserialize;
/**
* This file implements functions to read and write to a group of memcache
* servers.
*
* The goals of this storage class is to provide failover, redundancy 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.
*
* @package SimpleSAMLphp
{
/**
* Cache of the memcache servers we are using.
*
/**
* 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.
*/
Logger::debug("loading key $key from memcache");
$latestInfo = null;
$latestTime = 0.0;
$latestData = null;
$mustUpdate = false;
Jaime Perez Crespo
committed
$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;
Jaime Perez Crespo
committed
if ($up !== false) {
$allDown = false;
}
Jaime Perez Crespo
committed
$allDown = false;
/** @var string $serializedInfo */
$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)) {
'Retrieved invalid data from a memcache server. Data was not an array.'
);
continue;
}
if (!array_key_exists('timestamp', $info)) {
'Retrieved invalid data from a memcache server. Missing timestamp.'
);
continue;
}
if (!array_key_exists('data', $info)) {
'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) {
Jaime Perez Crespo
committed
if ($allDown) {
// all servers are down, panic!
$e = new Error\Error(ErrorCodes::MEMCACHEDOWN, null, 503);
throw new Error\Exception('All memcache servers are down', 503, $e);
Jaime Perez Crespo
committed
}
// we didn't find any data matching the key
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
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(string $key, mixed $value, ?int $expire = null): void
Logger::debug("saving key $key to memcache");
'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) {
}
}
/**
* Delete a key-value pair from the memcache servers.
*
* @param string $key The key we should delete.
*/
public static function delete(string $key): void
Logger::debug("deleting key $key from memcache");
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// 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 \Memcached $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(Memcached $memcache, array $server): void
{
// the hostname option is required
if (!array_key_exists('hostname', $server)) {
"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)) {
"Invalid hostname for server in the 'memcache_store.servers' configuration option. The hostname is" .
' supposed to be a string.'
);
}
// check if the user has specified a port number
// 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)) {
"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;
}
}
// add this server to the Memcache object
}
/**
* 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.
* @param string $index The index for this group. Specify if persistent connections are desired.
* @return \Memcached A Memcache object of the servers in the group
* @throws \Exception If the servers configuration is invalid.
private static function loadMemcacheServerGroup(array $group, $index = null)
}
if (array_key_exists('options', $group)) {
$memcache->setOptions($group['options']);
unset($group['options']);
}
$servers = $memcache->getServerList();
if (count($servers) === count($group) && !$memcache->isPristine()) {
return $memcache;
}
$memcache->resetServerList();
// 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)) {
"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)) {
'Invalid value for the server with index ' . $index .
'. Remember 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 \Memcached[] Array with Memcache objects.
* @throws \Exception If the servers configuration is invalid.
private static function getMemcacheServers(): array
{
// check if we have loaded the servers already
return self::$serverGroups;
}
// initialize the servers-array
$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 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)) {
"Invalid value for the server with index " . $index .
". Remember that the 'memcache_store.servers' configuration option" .
' contains an array of arrays of arrays.'
);
}
// make sure that the group doesn't have an index. An index would be a sign of invalid configuration
if (is_int($index)) {
$index = null;
}
// parse and add this group to the server group list
self::$serverGroups[] = self::loadMemcacheServerGroup($group, $index);
}
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 int 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(): int
{
// get the configuration instance
// get the expire-value from the configuration
$expire = $config->getOptionalInteger('memcache_store.expires', 0);
// it must be a positive integer
if ($expire < 0) {
"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.
*/
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.
*/
}
/**
* 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.
foreach (self::getMemcacheServers() as $sg) {
foreach ($stats as $server => $data) {
if ($data === false) {
throw new Exception('Failed to get memcache server status.');
$arrayUtils = new Utils\Arrays();
$stats = $arrayUtils->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.
*/
foreach (self::getMemcacheServers() as $sg) {
$ret[] = $stats;
}
return $ret;
}