Skip to content
Snippets Groups Projects
Commit 5bd214b8 authored by Olav Morken's avatar Olav Morken
Browse files

Initial implementation of MemcacheStore.

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@74 44740490-163a-0410-bde0-09ae8108e29a
parent fa6b9892
No related branches found
No related tags found
No related merge requests found
<?
/*
* This file implements a storage class which stores data to one or more
* groups 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.
*/
/* We need access to the configuration from config/config.php. */
require_once('SimpleSAML/Configuration.php');
class SimpleSAML_MemcacheStore {
/* This variable contains the last commit time of this object.
* This is used to determine which of the memcache servers contain
* the newest version.
*
* This variable is serialized.
*/
private $lastCommitTime = NULL;
/* This variable contains the id for this data.
*
* This variable is serialized.
*/
private $id = NULL;
/* This variable contains an array with all key-value pairs stored
* in this object.
*
* This variable is serialized.
*/
private $data = NULL;
/* This variable contains the serialized data which is currently
* stored on the memcache servers. By comparing the data which is
* stored against the current data, we can determine whether we
* should update the data.
*
* If this variable is NULL, then we need to store data to the
* memcache servers.
*
* This variable isn't serialized.
*/
private $savedData = NULL;
/* Private cache of the memcache servers we are using. */
private static $serverGroups = NULL;
/* This function is used to find an existing storage object. It will
* return NULL if no storage object with the given id is found.
*
* Parameters:
* $id The id of the storage object we are looking for. A id
* consists of lowercase alphanumeric characters.
*
* Returns:
* The corresponding MemcacheStorage object if the data is found
* or NULL if it isn't found.
*/
public static function find($id) {
assert(self::isValidID($id));
$mustUpdate = FALSE;
$latest = NULL;
$latestSerializedValue = NULL;
/* Search all the servers for the given id. */
foreach(self::getMemcacheServers() as $server) {
$serializedValue = $server->get($id);
if($serializedValue === FALSE) {
/* Either the server is down, or we don't have
* the value stored on that server.
*/
$mustUpdate = TRUE;
continue;
}
/* Deserialize the object. */
$v = unserialize($serializedValue);
/* Make sure that the deserialized object is of the
* correct type.
*/
if(!($v instanceof self)) {
$e = 'We retrieved an object from the' .
' memcache server which wasn\'t an' .
' instance of a MemcacheStore object.' .
' This should never happen, and is a ' .
' sign that the MemcacheStore may be ' .
' unreliable.';
error_log($e);
die($e);
}
if($latest === NULL) {
$latest = $v;
$latestSerializedValue = $serializedValue;
continue;
}
if($latest->lastCommitTime == $v->lastCommitTime) {
/* They were committed at the same time. Assume
* that they are equal.
*/
continue;
}
/* They are different. We need to update at least one
* of them to maintain synchronization.
*/
$mustUpdate = TRUE;
/* Update $latest if $v is newer than $latest. */
if($latest->lastCommitTime < $v->lastCommitTime) {
$latest = $v;
$latestSerializedValue = $serializedValue;
}
}
/* Check if we found any stored object matching the id. */
if($latest === NULL) {
return NULL;
}
/* If we don't need to update this data object, then we can
* store the serialized value in the MemcacheStore object. If
* we need to update this object, then we don't store the
* serialized value.
*/
if($mustUpdate === TRUE) {
$latest->savedData = $latestSerializedValue;
}
return $latest;
}
/* This constructor is used to create a new storage object. The storage
* object will be created with the specified id and the initial
* content passed in the data argument.
*
* If there exists a storage object with the specified id, then it will
* be overwritten.
*
* Parameters:
* $id The id of the storage object.
* $data An array containing the initial data of the storage
* object.
*/
public function __construct($id, $data = array()) {
/* Validate arguments. */
assert(self::isValidID($id));
assert(is_array($data));
$this->id = $id;
$this->data = $data;
/* Add a call to save the data when we exit. */
register_shutdown_function(array($this, 'save'));
}
/* This magic function is called on serialization of this class.
* It returns a list of the names of the variables which should be
* serialized.
*/
private function __sleep() {
return array('lastCommitTime', 'id', 'data');
}
/* This function retrieves the specified key from this storage object.
*
* Parameters:
* $key The key we should retrieve the value of.
*
* Returns:
* The value of the specified key, or NULL of the key wasn't found.
*/
public function get($key) {
if(!array_key_exists($key, $this->data)) {
return NULL;
}
return $this->data[$key];
}
/* This function sets the specified key to the specified value in this
* storage object.
*
* Parameters:
* $key The key we should set.
* $value The value we should set the key to.
*/
public function set($key, $value) {
$this->data[$key] = $value;
/* Set savedData to NULL. This will save time when
* we are going to decide whether we need to update this
* object on the memcache servers.
*/
$this->savedData = NULL;
}
/* This function determines whether we need to update the data which
* is stored on the memcache servers.
*
* If we are unable to detect a change, then we will serialize the
* class and compare this to the data we have cached. We do this to
* determine if any of the references have changed.
*
* Returns:
* TRUE if this object needs an update, FALSE if not.
*/
private function needUpdate() {
/* If $savedData is NULL, then we don't have any data stored
* on any servers. Therefore, we need to update the data.
*/
if($this->savedData === NULL) {
return TRUE;
}
/* Calculate the serialized value of this object. */
$serialized = serialize($this);
/* If the serialized value of this object matches the previous
* serialized value, then we don't need to update the data on
* the servers.
*/
if($serialized === $this->savedData) {
return FALSE;
}
/* We need to store the updated value to the servers. */
return TRUE;
}
/* This function stores this storage object to the memcache servers.
*/
public function save() {
/* First, chech whether we need to store new data. */
if(!$this->needUpdate()) {
/* This object is unchanged - we don't need to
* commit.
*/
return;
}
/* Update the last-commit timestamp. */
$this->lastCommitTime = time();
/* Calculate the value we should store on the servers. */
$this->savedData = serialize($this);
/* Store this object to all groups of memcache servers. */
foreach(self::getMemcacheServers() as $server) {
$server->set($this->id, $this->savedData);
}
}
/* This function adds a server from the 'memcache_store.servers'
* configuration option to a Memcache object.
*
* Parameters:
* $memcache The Memcache object we should add this server to.
* $server The server we should parse. This 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.
*/
private static function addMemcacheServer($memcache, $server) {
/* The hostname option is required. */
if(!array_key_exists('hostname', $server)) {
$e = 'hostname setting missing from server in the' .
' \'memcache_store.servers\' configuration' .
' option.';
error_log($e);
die($e);
}
$hostname = $server['hostname'];
/* The hostname must be a valid string. */
if(!is_string($hostname)) {
$e = 'Invalid hostname for server in the' .
' \'memcache_store.servers\' configuration' .
' option. The hostname is supposed to be a' .
' string.';
error_log($e);
die($e);
}
/* Check if the user has specified a port number. */
if(array_key_exists('port', $server)) {
/* Get the port number from the array, and validate
* it.
*/
$port = (int)$server['port'];
if(($port < 0) || ($port > 65535)) {
$e = 'Invalid port for server in the' .
' \'memcache_store.servers\'' .
' configuration option. The port number' .
' is supposed to be an integer between' .
' 0 and 65535.';
error_log($e);
die($e);
}
} 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) {
$e = 'Invalid weight for server in the' .
' \'memcache_store.servers\'' .
' configuration option. The weight is' .
' supposed to be a positive integer.';
error_log($e);
die($e);
}
} 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) {
$e = 'Invalid timeout for server in the' .
' \'memcache_store.servers\'' .
' configuration option. The timeout is' .
' supposed to be a positive integer.';
error_log($e);
die($e);
}
} else {
/* Use a default timeout of 3 seconds. */
$timeout = 3;
}
/* Add this server to the Memcache object. */
$memcache->addServer($hostname, $port, TRUE, $weight, $timeout);
}
/* This function takes in a list of servers belonging to a group and
* creates a Memcache object from the servers in the group.
*
* Parameters:
* $group Array of servers. Each server is represented by one array
* with the hostname (and optionally port number, timeout,
* ...). See the addMemcacheServer function for more
* information.
*
* Returns:
* A Memcache object of the servers in the group.
*/
private static function loadMemcacheServerGroup($group) {
/* Create the Memcache object. */
$memcache = new Memcache();
if($memcache == NULL) {
$e = 'Unable to create an instance of a Memcache' .
' object. Is the memcache extension' .
' installed?';
error_log($e);
die($e);
}
/* 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)) {
$e = '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;
error_log($e);
die($e);
}
/* Make sure that the server object is an array. Each
* server is an array with name-value pairs.
*/
if(!is_array($server)) {
$e = 'Invalid value for the server with' .
' index ' . $index . '. Remeber that' .
' the \'memcache_store.servers\'' .
' configuration option contains an' .
' array of arrays of arrays.';
error_log($e);
die($e);
}
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.
*
* Returns:
* Array with Memcache objects.
*/
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();
assert($config instanceof SimpleSAML_Configuration);
$groups = $config->getValue('memcache_store.servers');
/* Validate the 'memcache_store.servers' configuration
* option.
*/
if(is_null($groups)) {
$e = 'Unable to get value of the' .
' \'memcache_store.servers\' configuration' .
' option.';
error_log($e);
die($e);
}
if(!is_array($groups)) {
$e = 'The value of the \'memcache_store.servers\'' .
' configuration option isn\'t an array.';
error_log($e);
die($e);
}
/* 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)) {
$e = '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;
error_log($e);
die($e);
}
/* 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)) {
$e = 'Invalid value for the server with' .
' index ' . $index . '. Remeber that' .
' the \'memcache_store.servers\'' .
' configuration option contains an' .
' array of arrays of arrays.';
error_log($e);
die($e);
}
/* Parse and add this group to the server group list.
*/
self::$serverGroups[] =
self::loadMemcacheServerGroup($group);
}
return self::$serverGroups;
}
/* This function determines whether the argument is a valid id.
* A valid id is a string containing lowercase alphanumeric
* characters.
*
* Parameters:
* $id The id we should validate.
*
* Returns:
* TRUE if the id is valid, FALSE otherwise.
*/
private static function isValidID($id) {
if(!is_string($id)) {
return FALSE;
}
if(strlen($id) < 1) {
return FALSE;
}
if(preg_match('/[^0-9a-z]/', $id)) {
return FALSE;
}
return TRUE;
}
}
?>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment