1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
|
<?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;
/**
* 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) {
$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
$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)
{
if (!class_exists('Memcache')) {
throw new Exception('Missing Memcache class. Is the memcache extension installed?');
}
// create the Memcache object
$memcache = new Memcache();
// 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 = $sg->getExtendedStats();
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 = $sg->getExtendedStats();
$ret[] = $stats;
}
return $ret;
}
}
|