Merge branch 'session-locking' into develop

Conflicts:
	redis_session.c
	tests/RedisTest.php
This commit is contained in:
Pavlo Yatsukhnenko
2018-03-24 17:45:51 +02:00
10 changed files with 875 additions and 28 deletions
+1
View File
@@ -14,3 +14,4 @@ missing
autom4te.cache
mkinstalldirs
run-tests.php
idea/*
-5
View File
@@ -262,11 +262,6 @@ PHP_REDIS_API int redis_sock_read_multibulk_multi_reply_loop(
INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab,
int numElems);
#ifndef _MSC_VER
ZEND_BEGIN_MODULE_GLOBALS(redis)
ZEND_END_MODULE_GLOBALS(redis)
#endif
extern zend_module_entry redis_module_entry;
#define redis_module_ptr &redis_module_entry
+6 -4
View File
@@ -73,6 +73,12 @@ PHP_INI_BEGIN()
PHP_INI_ENTRY("redis.clusters.read_timeout", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.clusters.seeds", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.clusters.timeout", "", PHP_INI_ALL, NULL)
/* redis session */
PHP_INI_ENTRY("redis.session.locking_enabled", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.session.lock_expire", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.session.lock_retries", "", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("redis.session.lock_wait_time", "", PHP_INI_ALL, NULL)
PHP_INI_END()
/** {{{ Argument info for commands in redis 1.0 */
@@ -226,10 +232,6 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_kscan, 0, 0, 2)
ZEND_ARG_INFO(0, i_count)
ZEND_END_ARG_INFO()
#ifdef ZTS
ZEND_DECLARE_MODULE_GLOBALS(redis)
#endif
static zend_function_entry redis_functions[] = {
PHP_ME(Redis, __construct, arginfo_void, ZEND_ACC_CTOR | ZEND_ACC_PUBLIC)
PHP_ME(Redis, __destruct, arginfo_void, ZEND_ACC_DTOR | ZEND_ACC_PUBLIC)
+339 -10
View File
@@ -41,13 +41,40 @@
#include "SAPI.h"
#include "ext/standard/url.h"
/* HOST_NAME_MAX doesn't exist everywhere */
#ifndef HOST_NAME_MAX
#if defined(_POSIX_HOST_NAME_MAX)
#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
#elif defined(MAXHOSTNAMELEN)
#define HOST_NAME_MAX MAXHOSTNAMELEN
#else
#define HOST_NAME_MAX 255
#endif
#endif
/* Session lock LUA as well as its SHA1 hash */
#define LOCK_RELEASE_LUA_STR "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end"
#define LOCK_RELEASE_LUA_LEN (sizeof(LOCK_RELEASE_LUA_STR) - 1)
#define LOCK_RELEASE_SHA_STR "b70c2384248f88e6b75b9f89241a180f856ad852"
#define LOCK_RELEASE_SHA_LEN (sizeof(LOCK_RELEASE_SHA_STR) - 1)
/* Check if a response is the Redis +OK status response */
#define IS_REDIS_OK(r, len) (r != NULL && len == 3 && !memcmp(r, "+OK", 3))
ps_module ps_mod_redis = {
PS_MOD(redis)
PS_MOD_SID(redis)
};
ps_module ps_mod_redis_cluster = {
PS_MOD(rediscluster)
};
typedef struct {
zend_bool is_locked;
char *session_key;
char *lock_key;
char *lock_secret;
} redis_session_lock_status;
typedef struct redis_pool_member_ {
RedisSock *redis_sock;
@@ -65,6 +92,7 @@ typedef struct {
int count;
redis_pool_member *head;
redis_session_lock_status lock_status;
} redis_pool;
@@ -78,7 +106,6 @@ redis_pool_add(redis_pool *pool, RedisSock *redis_sock, int weight,
rpm->database = database;
rpm->prefix = prefix;
rpm->auth = auth;
rpm->next = pool->head;
@@ -101,9 +128,35 @@ redis_pool_free(redis_pool *pool TSRMLS_DC) {
efree(rpm);
rpm = next;
}
/* Cleanup after our lock */
if (pool->lock_status.session_key)
efree(pool->lock_status.session_key);
if (pool->lock_status.lock_secret)
efree(pool->lock_status.lock_secret);
if (pool->lock_status.lock_key)
efree(pool->lock_status.lock_key);
/* Cleanup pool itself */
efree(pool);
}
/* Send a command to Redis. Returns reply on success and NULL on failure */
static char *redis_simple_cmd(RedisSock *redis_sock, char *cmd, int cmdlen,
int *replylen TSRMLS_DC)
{
char *reply;
if (redis_sock_write(redis_sock, cmd, cmdlen TSRMLS_CC) >= 0) {
if ((reply = redis_sock_read(redis_sock, replylen TSRMLS_CC)) != NULL) {
return reply;
}
}
/* Failed to send or receive command */
return NULL;
}
static void
redis_pool_member_auth(redis_pool_member *rpm TSRMLS_DC) {
RedisSock *redis_sock = rpm->redis_sock;
@@ -171,6 +224,182 @@ redis_pool_get_sock(redis_pool *pool, const char *key TSRMLS_DC) {
return NULL;
}
/* Helper to set our session lock key */
static int set_session_lock_key(RedisSock *redis_sock, char *cmd, int cmd_len
TSRMLS_DC)
{
char *reply;
int reply_len;
reply = redis_simple_cmd(redis_sock, cmd, cmd_len, &reply_len TSRMLS_CC);
if (reply) {
if (IS_REDIS_OK(reply, reply_len)) {
efree(reply);
return SUCCESS;
}
efree(reply);
}
return FAILURE;
}
static int lock_acquire(RedisSock *redis_sock, redis_session_lock_status *lock_status
TSRMLS_DC)
{
char *cmd, hostname[HOST_NAME_MAX] = {0};
int cmd_len, lock_wait_time, retries, i, expiry;
/* Short circuit if we are already locked or not using session locks */
if (lock_status->is_locked || !INI_INT("redis.session.locking_enabled"))
return SUCCESS;
/* How long to wait between attempts to acquire lock */
lock_wait_time = INI_INT("redis.session.lock_wait_time");
if (lock_wait_time == 0) {
lock_wait_time = 2000;
}
/* Maximum number of times to retry (-1 means infinite) */
retries = INI_INT("redis.session.lock_retries");
if (retries == 0) {
retries = 10;
}
/* How long should the lock live (in seconds) */
expiry = INI_INT("redis.session.lock_expire");
if (expiry == 0) {
expiry = INI_INT("max_execution_time");
}
/* Generate our qualified lock key */
spprintf(&lock_status->lock_key, 0, "%s%s", lock_status->session_key, "_LOCK");
/* Calculate lock secret */
gethostname(hostname, HOST_NAME_MAX);
spprintf(&lock_status->lock_secret, 0, "%s|%ld", hostname, (long)getpid());
if (expiry > 0) {
cmd_len = REDIS_SPPRINTF(&cmd, "SET", "ssssd", lock_status->lock_key,
strlen(lock_status->lock_key), lock_status->lock_secret,
strlen(lock_status->lock_secret), "NX", 2,
"PX", 2, expiry * 1000);
} else {
cmd_len = REDIS_SPPRINTF(&cmd, "SET", "sss", lock_status->lock_key,
strlen(lock_status->lock_key), lock_status->lock_secret,
strlen(lock_status->lock_secret), "NX", 2);
}
/* Attempt to get our lock */
for (i = 0; retries == -1 || i <= retries; i++) {
if (set_session_lock_key(redis_sock, cmd, cmd_len TSRMLS_CC) == SUCCESS) {
lock_status->is_locked = 1;
break;
}
/* Sleep unless we're done making attempts */
if (retries == -1 || i < retries) {
usleep(lock_wait_time);
}
}
/* Cleanup SET command */
efree(cmd);
/* Success if we're locked */
return lock_status->is_locked ? SUCCESS : FAILURE;
}
#define IS_LOCK_SECRET(reply, len, secret) (len == strlen(secret) && !strncmp(reply, secret, len))
static void refresh_lock_status(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
{
char *cmd, *reply = NULL;
int replylen, cmdlen;
/* Return early if we're not locked */
if (!lock_status->is_locked)
return;
/* If redis.session.lock_expire is not set => TTL=max_execution_time
Therefore it is guaranteed that the current process is still holding
the lock */
if (lock_status->is_locked && INI_INT("redis.session.lock_expire") == 0)
return;
/* Command to get our lock key value and compare secrets */
cmdlen = REDIS_SPPRINTF(&cmd, "GET", "s", lock_status->lock_key,
strlen(lock_status->lock_key));
/* Attempt to refresh the lock */
reply = redis_simple_cmd(redis_sock, cmd, cmdlen, &replylen TSRMLS_CC);
if (reply != NULL) {
lock_status->is_locked = IS_LOCK_SECRET(reply, replylen, lock_status->lock_secret);
efree(reply);
} else {
lock_status->is_locked = 0;
}
/* Issue a warning if we're not locked. We don't attempt to refresh the lock
* if we aren't flagged as locked, so if we're not flagged here something
* failed */
if (!lock_status->is_locked) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to refresh session lock");
}
/* Cleanup */
efree(cmd);
}
static int write_allowed(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
{
if (!INI_INT("redis.session.locking_enabled"))
return 1;
refresh_lock_status(redis_sock, lock_status TSRMLS_CC);
return lock_status->is_locked;
}
/* Release any session lock we hold and cleanup allocated lock data. This function
* first attempts to use EVALSHA and then falls back to EVAL if EVALSHA fails. This
* will cause Redis to cache the script, so subsequent calls should then succeed
* using EVALSHA. */
static void lock_release(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
{
char *cmd, *reply;
int i, cmdlen, replylen;
/* Keywords, command, and length fallbacks */
const char *kwd[] = {"EVALSHA", "EVAL"};
const char *lua[] = {LOCK_RELEASE_SHA_STR, LOCK_RELEASE_LUA_STR};
int len[] = {LOCK_RELEASE_SHA_LEN, LOCK_RELEASE_LUA_LEN};
/* We first want to try EVALSHA and then fall back to EVAL */
for (i = 0; lock_status->is_locked && i < sizeof(kwd)/sizeof(*kwd); i++) {
/* Construct our command */
cmdlen = REDIS_SPPRINTF(&cmd, (char*)kwd[i], "sdss", lua[i], len[i], 1,
lock_status->lock_key, strlen(lock_status->lock_key),
lock_status->lock_secret, strlen(lock_status->lock_secret));
/* Send it off */
reply = redis_simple_cmd(redis_sock, cmd, cmdlen, &replylen TSRMLS_CC);
/* Release lock and cleanup reply if we got one */
if (reply != NULL) {
lock_status->is_locked = 0;
efree(reply);
}
/* Cleanup command */
efree(cmd);
}
/* Something has failed if we are still locked */
if (lock_status->is_locked) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Failed to release session lock");
}
}
/* {{{ PS_OPEN_FUNC
*/
PS_OPEN_FUNC(redis)
@@ -189,7 +418,7 @@ PS_OPEN_FUNC(redis)
/* find end of url */
j = i;
while (j<path_len && !isspace(save_path[j]) && save_path[j] != ',')
j++;
j++;
if (i < j) {
int weight = 1;
@@ -197,8 +426,8 @@ PS_OPEN_FUNC(redis)
int persistent = 0;
int database = -1;
char *persistent_id = NULL;
zend_string *prefix = NULL, *auth = NULL;
long retry_interval = 0;
zend_string *prefix = NULL, *auth = NULL;
/* translate unix: into file: */
if (!strncmp(save_path+i, "unix:", sizeof("unix:")-1)) {
@@ -308,10 +537,18 @@ PS_CLOSE_FUNC(redis)
{
redis_pool *pool = PS_GET_MOD_DATA();
if (pool){
if (pool) {
redis_pool_member *rpm = redis_pool_get_sock(pool, pool->lock_status.session_key TSRMLS_CC);
RedisSock *redis_sock = rpm ? rpm->redis_sock : NULL;
if (redis_sock) {
lock_release(redis_sock, &pool->lock_status TSRMLS_CC);
}
redis_pool_free(pool TSRMLS_CC);
PS_SET_MOD_DATA(NULL);
}
return SUCCESS;
}
/* }}} */
@@ -338,6 +575,73 @@ redis_session_key(redis_pool_member *rpm, const char *key, int key_len, int *ses
return session;
}
/* {{{ PS_CREATE_SID_FUNC
*/
PS_CREATE_SID_FUNC(redis)
{
int retries = 3;
redis_pool *pool = PS_GET_MOD_DATA();
if (!pool) {
#if (PHP_MAJOR_VERSION < 7)
return php_session_create_id(NULL, newlen TSRMLS_CC);
#else
return php_session_create_id(NULL TSRMLS_CC);
#endif
}
while (retries-- > 0) {
#if (PHP_MAJOR_VERSION < 7)
char* sid = php_session_create_id((void **) &pool, newlen TSRMLS_CC);
redis_pool_member *rpm = redis_pool_get_sock(pool, sid TSRMLS_CC);
#else
zend_string* sid = php_session_create_id((void **) &pool TSRMLS_CC);
redis_pool_member *rpm = redis_pool_get_sock(pool, ZSTR_VAL(sid) TSRMLS_CC);
#endif
RedisSock *redis_sock = rpm?rpm->redis_sock:NULL;
if (!rpm || !redis_sock) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE,
"Redis not available while creating session_id");
#if (PHP_MAJOR_VERSION < 7)
efree(sid);
return php_session_create_id(NULL, newlen TSRMLS_CC);
#else
zend_string_release(sid);
return php_session_create_id(NULL TSRMLS_CC);
#endif
}
int resp_len;
#if (PHP_MAJOR_VERSION < 7)
char *full_session_key = redis_session_key(rpm, sid, strlen(sid), &resp_len);
#else
char *full_session_key = redis_session_key(rpm, ZSTR_VAL(sid), ZSTR_LEN(sid), &resp_len);
#endif
char *full_session_key_nt = estrndup(full_session_key, resp_len);
efree(full_session_key);
pool->lock_status.session_key = full_session_key_nt;
if (lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) == SUCCESS) {
return sid;
}
#if (PHP_MAJOR_VERSION < 7)
efree(sid);
#else
zend_string_release(sid);
#endif
sid = NULL;
}
php_error_docref(NULL TSRMLS_CC, E_NOTICE,
"Acquiring session lock failed while creating session_id");
return NULL;
}
/* }}} */
/* {{{ PS_READ_FUNC
*/
PS_READ_FUNC(redis)
@@ -363,9 +667,15 @@ PS_READ_FUNC(redis)
/* send GET command */
resp = redis_session_key(rpm, skey, skeylen, &resp_len);
pool->lock_status.session_key = estrndup(resp, resp_len);
cmd_len = REDIS_SPPRINTF(&cmd, "GET", "s", resp, resp_len);
efree(resp);
if (lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) != SUCCESS) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE,
"Acquire of session lock was not successful");
}
if (redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) {
efree(cmd);
return FAILURE;
@@ -417,17 +727,31 @@ PS_WRITE_FUNC(redis)
redis_pool *pool = PS_GET_MOD_DATA();
redis_pool_member *rpm = redis_pool_get_sock(pool, skey TSRMLS_CC);
RedisSock *redis_sock = rpm?rpm->redis_sock:NULL;
if (!rpm || !redis_sock){
RedisSock *redis_sock = rpm ? rpm->redis_sock : NULL;
if (!redis_sock) {
return FAILURE;
}
/* send SET command */
session = redis_session_key(rpm, skey, skeylen, &session_len);
#if (PHP_MAJOR_VERSION < 7)
/* We need to check for PHP5 if the session key changes (a bug with session_regenerate_id() is causing a missing PS_CREATE_SID call)*/
int session_key_changed = strlen(pool->lock_status.session_key) != session_len || strncmp(pool->lock_status.session_key, session, session_len) != 0;
if (session_key_changed) {
efree(pool->lock_status.session_key);
pool->lock_status.session_key = estrndup(session, session_len);
}
if (session_key_changed && lock_acquire(redis_sock, &pool->lock_status TSRMLS_CC) != SUCCESS) {
efree(session);
return FAILURE;
}
#endif
cmd_len = REDIS_SPPRINTF(&cmd, "SETEX", "sds", session, session_len,
INI_INT("session.gc_maxlifetime"), sval, svallen);
efree(session);
if (redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) {
if (!write_allowed(redis_sock, &pool->lock_status TSRMLS_CC) || redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) {
efree(cmd);
return FAILURE;
}
@@ -438,7 +762,7 @@ PS_WRITE_FUNC(redis)
return FAILURE;
}
if (response_len == 3 && strncmp(response, "+OK", 3) == 0) {
if (IS_REDIS_OK(response, response_len)) {
efree(response);
return SUCCESS;
} else {
@@ -469,6 +793,11 @@ PS_DESTROY_FUNC(redis)
return FAILURE;
}
/* Release lock */
if (redis_sock) {
lock_release(redis_sock, &pool->lock_status TSRMLS_CC);
}
/* send DEL command */
session = redis_session_key(rpm, skey, skeylen, &session_len);
cmd_len = REDIS_SPPRINTF(&cmd, "DEL", "s", session, session_len);
+1 -1
View File
@@ -9,6 +9,7 @@ PS_READ_FUNC(redis);
PS_WRITE_FUNC(redis);
PS_DESTROY_FUNC(redis);
PS_GC_FUNC(redis);
PS_CREATE_SID_FUNC(redis);
PS_OPEN_FUNC(rediscluster);
PS_CLOSE_FUNC(rediscluster);
@@ -19,4 +20,3 @@ PS_GC_FUNC(rediscluster);
#endif
#endif
+20
View File
@@ -23,6 +23,11 @@ class Redis_Cluster_Test extends Redis_Test {
RedisCluster::FAILOVER_DISTRIBUTE
);
/**
* @var string
*/
protected $sessionPrefix = 'PHPREDIS_CLUSTER_SESSION:';
/* Tests we'll skip all together in the context of RedisCluster. The
* RedisCluster class doesn't implement specialized (non-redis) commands
* such as sortAsc, or sortDesc and other commands such as SELECT are
@@ -37,6 +42,21 @@ class Redis_Cluster_Test extends Redis_Test {
public function testSwapDB() { return $this->markTestSkipped(); }
public function testConnectException() { return $this->markTestSkipped(); }
/* Session locking feature is currently not supported in in context of Redis Cluster.
The biggest issue for this is the distribution nature of Redis cluster */
public function testSession_savedToRedis() { return $this->markTestSkipped(); }
public function testSession_lockKeyCorrect() { return $this->markTestSkipped(); }
public function testSession_lockingDisabledByDefault() { return $this->markTestSkipped(); }
public function testSession_lockReleasedOnClose() { return $this->markTestSkipped(); }
public function testSession_ttlMaxExecutionTime() { return $this->markTestSkipped(); }
public function testSession_ttlLockExpire() { return $this->markTestSkipped(); }
public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock() { return $this->markTestSkipped(); }
public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock() { return $this->markTestSkipped(); }
public function testSession_correctLockRetryCount() { return $this->markTestSkipped(); }
public function testSession_defaultLockRetryCount() { return $this->markTestSkipped(); }
public function testSession_noUnlockOfOtherProcess() { return $this->markTestSkipped(); }
public function testSession_lockWaitTime() { return $this->markTestSkipped(); }
/* Load our seeds on construction */
public function __construct() {
$str_nodemap_file = dirname($_SERVER['PHP_SELF']) . '/nodes/nodemap';
+368 -8
View File
@@ -21,6 +21,11 @@ class Redis_Test extends TestSuite
*/
public $redis;
/**
* @var string
*/
protected $sessionPrefix = 'PHPREDIS_SESSION:';
public function setUp() {
$this->redis = $this->newInstance();
$info = $this->redis->info();
@@ -5142,15 +5147,176 @@ class Redis_Test extends TestSuite
$this->assertEquals($this->redis->lrange('mylist', 0, -1), Array('A','B','C','D'));
}
public function testSession()
public function testSession_savedToRedis()
{
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://localhost:6379');
if (!@session_start()) {
return $this->markTestSkipped();
}
session_write_close();
$this->assertTrue($this->redis->exists('PHPREDIS_SESSION:' . session_id()));
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false);
$this->assertTrue($this->redis->exists($this->sessionPrefix . $sessionId));
$this->assertTrue($sessionSuccessful);
}
public function testSession_lockKeyCorrect()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 5, true);
usleep(100000);
$this->assertTrue($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK'));
}
public function testSession_lockingDisabledByDefault()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 5, true, 300, false);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, false);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertFalse($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK'));
$this->assertTrue($elapsedTime < 1);
$this->assertTrue($sessionSuccessful);
}
public function testSession_lockReleasedOnClose()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 1, true);
usleep(1100000);
$this->assertFalse($this->redis->exists($this->sessionPrefix . $sessionId . '_LOCK'));
}
public function testSession_ttlMaxExecutionTime()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 10, true, 2);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime < 3);
$this->assertTrue($sessionSuccessful);
}
public function testSession_ttlLockExpire()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 10, true, 300, true, null, -1, 2);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime < 3);
$this->assertTrue($sessionSuccessful);
}
public function testSession_lockHoldCheckBeforeWrite_otherProcessHasLock()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 2, true, 300, true, null, -1, 1, 'firstProcess');
usleep(1500000); // 1.5 sec
$writeSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 10, 'secondProcess');
sleep(1);
$this->assertTrue($writeSuccessful);
$this->assertEquals('secondProcess', $this->getSessionData($sessionId));
}
public function testSession_lockHoldCheckBeforeWrite_nobodyHasLock()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$writeSuccessful = $this->startSessionProcess($sessionId, 2, false, 300, true, null, -1, 1, 'firstProcess');
$this->assertFalse($writeSuccessful);
$this->assertTrue('firstProcess' !== $this->getSessionData($sessionId));
}
public function testSession_correctLockRetryCount()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 10, true);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 10, true, 1000000, 3);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime > 3 && $elapsedTime < 4);
$this->assertFalse($sessionSuccessful);
}
public function testSession_defaultLockRetryCount()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 10, true);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 10, true, 200000, 0);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime > 2 && $elapsedTime < 3);
$this->assertFalse($sessionSuccessful);
}
public function testSession_noUnlockOfOtherProcess()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 3, true, 1); // Process 1
usleep(100000);
$this->startSessionProcess($sessionId, 5, true); // Process 2
$start = microtime(true);
// Waiting until TTL of process 1 ended and process 2 locked the session,
// because is not guaranteed which waiting process gets the next lock
sleep(1);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime > 5);
$this->assertTrue($sessionSuccessful);
}
public function testSession_lockWaitTime()
{
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 1, true, 300);
usleep(100000);
$start = microtime(true);
$sessionSuccessful = $this->startSessionProcess($sessionId, 0, false, 300, true, 3000000);
$end = microtime(true);
$elapsedTime = $end - $start;
$this->assertTrue($elapsedTime > 2.5);
$this->assertTrue($elapsedTime < 3.5);
$this->assertTrue($sessionSuccessful);
}
public function testMultipleConnect() {
@@ -5175,5 +5341,199 @@ class Redis_Test extends TestSuite
$this->assertTrue(strpos($e, "timed out") !== false);
}
}
public function testSession_regenerateSessionId_noLock_noDestroy() {
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_noLock_withDestroy() {
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, false, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_withLock_noDestroy() {
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_withLock_withDestroy() {
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, true, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_noLock_noDestroy_withProxy() {
if (!interface_exists('SessionHandlerInterface')) {
$this->markTestSkipped('session handler interface not available in PHP < 5.4');
}
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, false, false, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_noLock_withDestroy_withProxy() {
if (!interface_exists('SessionHandlerInterface')) {
$this->markTestSkipped('session handler interface not available in PHP < 5.4');
}
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, false, true, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_withLock_noDestroy_withProxy() {
if (!interface_exists('SessionHandlerInterface')) {
$this->markTestSkipped('session handler interface not available in PHP < 5.4');
}
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, true, false, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
public function testSession_regenerateSessionId_withLock_withDestroy_withProxy() {
if (!interface_exists('SessionHandlerInterface')) {
$this->markTestSkipped('session handler interface not available in PHP < 5.4');
}
$this->setSessionHandler();
$sessionId = $this->generateSessionId();
$this->startSessionProcess($sessionId, 0, false, 300, true, null, -1, 1, 'bar');
$newSessionId = $this->regenerateSessionId($sessionId, true, true, true);
$this->assertTrue($newSessionId !== $sessionId);
$this->assertEquals('bar', $this->getSessionData($newSessionId));
}
private function setSessionHandler()
{
$host = $this->getHost() ?: 'localhost';
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://' . $host . ':6379');
}
/**
* @return string
*/
private function generateSessionId()
{
if (function_exists('session_create_id')) {
return session_create_id();
} else {
$encoded = bin2hex(openssl_random_pseudo_bytes(8));
return $encoded;
}
}
/**
* @param string $sessionId
* @param int $sleepTime
* @param bool $background
* @param int $maxExecutionTime
* @param bool $locking_enabled
* @param int $lock_wait_time
* @param int $lock_retries
* @param int $lock_expires
* @param string $sessionData
*
* @return bool
*/
private function startSessionProcess($sessionId, $sleepTime, $background, $maxExecutionTime = 300, $locking_enabled = true, $lock_wait_time = null, $lock_retries = -1, $lock_expires = 0, $sessionData = '')
{
if (substr(php_uname(), 0, 7) == "Windows"){
$this->markTestSkipped();
return true;
} else {
$commandParameters = array($this->getHost(), $sessionId, $sleepTime, $maxExecutionTime, $lock_retries, $lock_expires, $sessionData);
if ($locking_enabled) {
$commandParameters[] = '1';
if ($lock_wait_time != null) {
$commandParameters[] = $lock_wait_time;
}
}
$commandParameters = array_map('escapeshellarg', $commandParameters);
$command = 'php ' . __DIR__ . '/startSession.php ' . implode(' ', $commandParameters);
$command .= $background ? ' 2>/dev/null > /dev/null &' : ' 2>&1';
exec($command, $output);
return ($background || (count($output) == 1 && $output[0] == 'SUCCESS')) ? true : false;
}
}
/**
* @param string $sessionId
*
* @return string
*/
private function getSessionData($sessionId)
{
$command = 'php ' . __DIR__ . '/getSessionData.php ' . escapeshellarg($this->getHost()) . ' ' . escapeshellarg($sessionId);
exec($command, $output);
return $output[0];
}
/**
* @param string $sessionId
* @param bool $locking
* @param bool $destroyPrevious
* @param bool $sessionProxy
*
* @return string
*/
private function regenerateSessionId($sessionId, $locking = false, $destroyPrevious = false, $sessionProxy = false)
{
$args = array_map('escapeshellarg', array($sessionId, $locking, $destroyPrevious, $sessionProxy));
$command = 'php --no-php-ini --define extension=igbinary.so --define extension=' . __DIR__ . '/../modules/redis.so ' . __DIR__ . '/regenerateSessionId.php ' . escapeshellarg($this->getHost()) . ' ' . implode(' ', $args);
exec($command, $output);
return $output[0];
}
}
?>
+19
View File
@@ -0,0 +1,19 @@
<?php
error_reporting(E_ERROR | E_WARNING);
$redisHost = $argv[1];
$sessionId = $argv[2];
if (empty($redisHost)) {
$redisHost = 'localhost';
}
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://' . $redisHost . ':6379');
session_id($sessionId);
if (!session_start()) {
echo "session_start() was nut successful";
} else {
echo isset($_SESSION['redis_test']) ? $_SESSION['redis_test'] : 'Key redis_test not found';
}
+83
View File
@@ -0,0 +1,83 @@
<?php
error_reporting(E_ERROR | E_WARNING);
$redisHost = $argv[1];
$sessionId = $argv[2];
$locking = !!$argv[3];
$destroyPrevious = !!$argv[4];
$sessionProxy = !!$argv[5];
if (empty($redisHost)) {
$redisHost = 'localhost';
}
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://' . $redisHost . ':6379');
if ($locking) {
ini_set('redis.session.locking_enabled', true);
}
if (interface_exists('SessionHandlerInterface')) {
class TestHandler implements SessionHandlerInterface
{
/**
* @var SessionHandler
*/
private $handler;
public function __construct()
{
$this->handler = new SessionHandler();
}
public function close()
{
return $this->handler->close();
}
public function destroy($session_id)
{
return $this->handler->destroy($session_id);
}
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}
public function open($save_path, $name)
{
return $this->handler->open($save_path, $name);
}
public function read($session_id)
{
return $this->handler->read($session_id);
}
public function write($session_id, $session_data)
{
return $this->handler->write($session_id, $session_data);
}
}
}
if ($sessionProxy) {
$handler = new TestHandler();
session_set_save_handler($handler);
}
session_id($sessionId);
if (!session_start()) {
$result = "FAILED: session_start()";
}
elseif (!session_regenerate_id($destroyPrevious)) {
$result = "FAILED: session_regenerate_id()";
}
else {
$result = session_id();
}
session_write_close();
echo $result;
+38
View File
@@ -0,0 +1,38 @@
<?php
error_reporting(E_ERROR | E_WARNING);
$redisHost = $argv[1];
$sessionId = $argv[2];
$sleepTime = $argv[3];
$maxExecutionTime = $argv[4];
$lock_retries = $argv[5];
$lock_expire = $argv[6];
$sessionData = $argv[7];
if (empty($redisHost)) {
$redisHost = 'localhost';
}
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://' . $redisHost . ':6379');
ini_set('max_execution_time', $maxExecutionTime);
ini_set('redis.session.lock_retries', $lock_retries);
ini_set('redis.session.lock_expire', $lock_expire);
if (isset($argv[8])) {
ini_set('redis.session.locking_enabled', $argv[8]);
}
if (isset($argv[9])) {
ini_set('redis.session.lock_wait_time', $argv[9]);
}
session_id($sessionId);
$sessionStartSuccessful = session_start();
sleep($sleepTime);
if (!empty($sessionData)) {
$_SESSION['redis_test'] = $sessionData;
}
session_write_close();
echo $sessionStartSuccessful ? 'SUCCESS' : 'FAILURE';