Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/phpredis/phpredis.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--common.h1
-rw-r--r--php_redis.h10
-rw-r--r--redis.c21
-rw-r--r--redis_session.c197
-rw-r--r--redis_session.h15
-rw-r--r--tests/RedisClusterTest.php20
-rw-r--r--tests/RedisTest.php252
-rw-r--r--tests/getSessionData.php17
-rw-r--r--tests/startSession.php36
10 files changed, 548 insertions, 22 deletions
diff --git a/.gitignore b/.gitignore
index 046241a9..0462ff37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ missing
autom4te.cache
mkinstalldirs
run-tests.php
+idea/* \ No newline at end of file
diff --git a/common.h b/common.h
index 708b1e1e..f2c1c247 100644
--- a/common.h
+++ b/common.h
@@ -15,6 +15,7 @@ typedef smart_str smart_string;
#define smart_string_appendc(dest, c) smart_str_appendc(dest, c)
#define smart_string_append_long(dest, val) smart_str_append_long(dest, val)
#define smart_string_appendl(dest, src, len) smart_str_appendl(dest, src, len)
+#define smart_string_free(str) smart_str_free(str)
typedef struct {
short gc;
diff --git a/php_redis.h b/php_redis.h
index 895ad710..930346ad 100644
--- a/php_redis.h
+++ b/php_redis.h
@@ -260,9 +260,17 @@ 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)
+ int lock_release_lua_script_uploaded;
+ char lock_release_lua_script_hash[41];
ZEND_END_MODULE_GLOBALS(redis)
+
+ZEND_EXTERN_MODULE_GLOBALS(redis);
+
+#ifdef ZTS
+#define REDIS_G(v) TSRMG(redis_globals_id, zend_redis_globals *, v)
+#else
+#define REDIS_G(v) (redis_globals.v)
#endif
extern zend_module_entry redis_module_entry;
diff --git a/redis.c b/redis.c
index df18decd..6914e12d 100644
--- a/redis.c
+++ b/redis.c
@@ -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_wait_time", "", PHP_INI_ALL, NULL)
+ PHP_INI_ENTRY("redis.session.lock_retries", "", PHP_INI_ALL, NULL)
+ PHP_INI_ENTRY("redis.session.lock_expire", "", PHP_INI_ALL, NULL)
PHP_INI_END()
/** {{{ Argument info for commands in redis 1.0 */
@@ -226,9 +232,7 @@ 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)
@@ -454,6 +458,13 @@ static const zend_module_dep redis_deps[] = {
ZEND_MOD_END
};
+static
+PHP_GINIT_FUNCTION(redis)
+{
+ redis_globals->lock_release_lua_script_uploaded = 0;
+ memset(redis_globals->lock_release_lua_script_hash, 0, 41);
+}
+
zend_module_entry redis_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER_EX,
@@ -470,7 +481,11 @@ zend_module_entry redis_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
PHP_REDIS_VERSION,
#endif
- STANDARD_MODULE_PROPERTIES
+ PHP_MODULE_GLOBALS(redis),
+ PHP_GINIT(redis),
+ NULL,
+ NULL,
+ STANDARD_MODULE_PROPERTIES_EX,
};
#ifdef COMPILE_DL_REDIS
diff --git a/redis_session.c b/redis_session.c
index fe753e7b..3be978cb 100644
--- a/redis_session.c
+++ b/redis_session.c
@@ -70,6 +70,7 @@ typedef struct {
int count;
redis_pool_member *head;
+ redis_session_lock_status *lock_status;
} redis_pool;
@@ -113,6 +114,7 @@ redis_pool_free(redis_pool *pool TSRMLS_DC) {
efree(rpm);
rpm = next;
}
+ efree(pool->lock_status);
efree(pool);
}
@@ -183,6 +185,162 @@ redis_pool_get_sock(redis_pool *pool, const char *key TSRMLS_DC) {
return NULL;
}
+int lock_acquire(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
+{
+ if (lock_status->is_locked || !INI_INT("redis.session.locking_enabled")) return SUCCESS;
+
+ char *cmd, *response;
+ int response_len, cmd_len, lock_wait_time, max_lock_retries, i_lock_retry, lock_expire;
+ calculate_lock_secret(lock_status);
+
+ lock_wait_time = INI_INT("redis.session.lock_wait_time");
+ if (lock_wait_time == 0) {
+ lock_wait_time = 2000;
+ }
+
+ max_lock_retries = INI_INT("redis.session.lock_retries");
+ if (max_lock_retries == 0) {
+ max_lock_retries = 10;
+ }
+
+ lock_expire = INI_INT("redis.session.lock_expire");
+ if (lock_expire == 0) {
+ lock_expire = INI_INT("max_execution_time");
+ }
+
+ // Building the redis lock key
+ smart_string_appendl(&lock_status->lock_key, lock_status->session_key, strlen(lock_status->session_key));
+ smart_string_appendl(&lock_status->lock_key, "_LOCK", strlen("_LOCK"));
+ smart_string_0(&lock_status->lock_key);
+
+ if (lock_expire > 0) {
+ cmd_len = REDIS_SPPRINTF(&cmd, "SET", "ssssd", lock_status->lock_key.c, lock_status->lock_key.len, lock_status->lock_secret.c, lock_status->lock_secret.len, "NX", 2, "PX", 2, lock_expire * 1000);
+ } else {
+ cmd_len = REDIS_SPPRINTF(&cmd, "SET", "sss", lock_status->lock_key.c, lock_status->lock_key.len, lock_status->lock_secret.c, lock_status->lock_secret.len, "NX", 2);
+ }
+
+ for (i_lock_retry = 0; !lock_status->is_locked && (max_lock_retries == -1 || i_lock_retry <= max_lock_retries); i_lock_retry++) {
+ if(!(redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0)
+ && ((response = redis_sock_read(redis_sock, &response_len TSRMLS_CC)) != NULL)
+ && response_len == 3
+ && strncmp(response, "+OK", 3) == 0) {
+ lock_status->is_locked = 1;
+ } else if (max_lock_retries == -1 || i_lock_retry < max_lock_retries) {
+ usleep(lock_wait_time);
+ }
+ }
+
+ if (response != NULL) {
+ efree(response);
+ }
+ efree(cmd);
+
+ if (lock_status->is_locked) {
+ return SUCCESS;
+ } else {
+ return FAILURE;
+ }
+}
+
+void refresh_lock_status(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
+{
+ 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;
+
+ char *cmd, *response;
+ int response_len, cmd_len;
+
+ cmd_len = REDIS_SPPRINTF(&cmd, "GET", "s", lock_status->lock_key.c, lock_status->lock_key.len);
+
+ redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC);
+ response = redis_sock_read(redis_sock, &response_len TSRMLS_CC);
+
+ if (response != NULL) {
+ lock_status->is_locked = (strcmp(response, lock_status->lock_secret.c) == 0);
+ efree(response);
+ } else {
+ lock_status->is_locked = 0;
+ }
+ efree(cmd);
+}
+
+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;
+}
+
+void lock_release(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC)
+{
+ if (lock_status->is_locked) {
+ char *cmd, *response;
+ int response_len, cmd_len;
+
+ upload_lock_release_script(redis_sock TSRMLS_CC);
+ cmd_len = REDIS_SPPRINTF(&cmd, "EVALSHA", "sdss", REDIS_G(lock_release_lua_script_hash), strlen(REDIS_G(lock_release_lua_script_hash)), 1, lock_status->lock_key.c, lock_status->lock_key.len, lock_status->lock_secret.c, lock_status->lock_secret.len);
+
+ redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC);
+ response = redis_sock_read(redis_sock, &response_len TSRMLS_CC);
+
+ // in case of redis script cache has been flushed
+ if (response == NULL) {
+ REDIS_G(lock_release_lua_script_uploaded) = 0;
+ upload_lock_release_script(redis_sock TSRMLS_CC);
+ redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC);
+ response = redis_sock_read(redis_sock, &response_len TSRMLS_CC);
+ lock_status->is_locked = 0;
+ }
+
+ if (response != NULL) {
+ efree(response);
+ }
+
+ efree(cmd);
+ }
+ smart_string_free(&lock_status->lock_key);
+ smart_string_free(&lock_status->lock_secret);
+}
+
+void upload_lock_release_script(RedisSock *redis_sock TSRMLS_DC)
+{
+ if (REDIS_G(lock_release_lua_script_uploaded)) return;
+
+ char *cmd, *response, *release_script;
+ int response_len, cmd_len;
+ release_script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
+
+ cmd_len = REDIS_SPPRINTF(&cmd, "SCRIPT", "ss", "LOAD", strlen("LOAD"), release_script, strlen(release_script));
+
+ redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC);
+ response = redis_sock_read(redis_sock, &response_len TSRMLS_CC);
+
+ if (response != NULL) {
+ memset(REDIS_G(lock_release_lua_script_hash), 0, 41);
+ strncpy(REDIS_G(lock_release_lua_script_hash), response, strlen(response));
+
+ REDIS_G(lock_release_lua_script_uploaded) = 1;
+ efree(response);
+ }
+
+ efree(cmd);
+}
+
+void calculate_lock_secret(redis_session_lock_status *lock_status)
+{
+ char hostname[64] = {0};
+ gethostname(hostname, 64);
+
+ // Concatenating the redis lock secret
+ smart_string_appendl(&lock_status->lock_secret, hostname, strlen(hostname));
+ smart_string_appendc(&lock_status->lock_secret, '|');
+ smart_string_append_long(&lock_status->lock_secret, getpid());
+ smart_string_0(&lock_status->lock_secret);
+}
+
/* {{{ PS_OPEN_FUNC
*/
PS_OPEN_FUNC(redis)
@@ -192,6 +350,9 @@ PS_OPEN_FUNC(redis)
int i, j, path_len;
redis_pool *pool = redis_pool_new(TSRMLS_C);
+ redis_session_lock_status *lock_status = ecalloc(1, sizeof(redis_session_lock_status));
+ lock_status->is_locked = 0;
+ pool->lock_status = lock_status;
for (i=0,j=0,path_len=strlen(save_path); i<path_len; i=j+1) {
/* find beginning of url */
@@ -308,9 +469,17 @@ PS_CLOSE_FUNC(redis)
redis_pool *pool = PS_GET_MOD_DATA();
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;
}
/* }}} */
@@ -361,9 +530,14 @@ PS_READ_FUNC(redis)
#else
resp = redis_session_key(rpm, ZSTR_VAL(key), ZSTR_LEN(key), &resp_len);
#endif
- cmd_len = REDIS_SPPRINTF(&cmd, "GET", "s", resp, resp_len);
+ pool->lock_status->session_key = (char *) emalloc(resp_len + 1);
+ memset(pool->lock_status->session_key, 0, resp_len + 1);
+ strncpy(pool->lock_status->session_key, resp, resp_len);
+ cmd_len = REDIS_SPPRINTF(&cmd, "GET", "s", resp, resp_len);
efree(resp);
+
+ lock_acquire(redis_sock, pool->lock_status TSRMLS_CC);
if(redis_sock_write(redis_sock, cmd, cmd_len TSRMLS_CC) < 0) {
efree(cmd);
return FAILURE;
@@ -427,7 +601,8 @@ PS_WRITE_FUNC(redis)
ZSTR_VAL(val), ZSTR_LEN(val));
#endif
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;
}
@@ -466,6 +641,11 @@ PS_DESTROY_FUNC(redis)
return FAILURE;
}
+ /* Release lock */
+ if (redis_sock) {
+ lock_release(redis_sock, pool->lock_status TSRMLS_CC);
+ }
+
/* send DEL command */
#if (PHP_MAJOR_VERSION < 7)
session = redis_session_key(rpm, key, strlen(key), &session_len);
@@ -521,7 +701,7 @@ static void session_conf_timeout(HashTable *ht_conf, const char *key, int key_le
}
/* Simple helper to retreive a boolean (0 or 1) value from a string stored in our
- * session.save_path variable. This is so the user can use 0, 1, or 'true',
+ * session.save_path variable. This is so the user can use 0, 1, or 'true',
* 'false' */
static void session_conf_bool(HashTable *ht_conf, char *key, int keylen,
int *retval) {
@@ -529,7 +709,7 @@ static void session_conf_bool(HashTable *ht_conf, char *key, int keylen,
char *str;
int strlen;
- /* See if we have the option, and it's a string */
+ /* See if we have the option, and it's a string */
if ((z_val = zend_hash_str_find(ht_conf, key, keylen - 1)) != NULL &&
Z_TYPE_P(z_val) == IS_STRING
) {
@@ -544,7 +724,7 @@ static void session_conf_bool(HashTable *ht_conf, char *key, int keylen,
}
/* Prefix a session key */
-static char *cluster_session_key(redisCluster *c, const char *key, int keylen,
+static char *cluster_session_key(redisCluster *c, const char *key, int keylen,
int *skeylen, short *slot) {
char *skey;
@@ -552,7 +732,6 @@ static char *cluster_session_key(redisCluster *c, const char *key, int keylen,
skey = emalloc(*skeylen);
memcpy(skey, ZSTR_VAL(c->flags->prefix), ZSTR_LEN(c->flags->prefix));
memcpy(skey + ZSTR_LEN(c->flags->prefix), key, keylen);
-
*slot = cluster_hash_key(skey, *skeylen);
return skey;
@@ -590,7 +769,7 @@ PS_OPEN_FUNC(rediscluster) {
/* Grab persistent option */
session_conf_bool(ht_conf, "persistent", sizeof("persistent"), &persistent);
-
+
/* Sanity check on our timeouts */
if (timeout < 0 || read_timeout < 0) {
php_error_docref(NULL TSRMLS_CC, E_WARNING,
@@ -635,7 +814,7 @@ PS_OPEN_FUNC(rediscluster) {
/* Cleanup */
zval_dtor(&z_conf);
-
+
return retval;
}
@@ -775,7 +954,7 @@ PS_DESTROY_FUNC(rediscluster) {
reply = cluster_read_resp(c TSRMLS_CC);
if (!reply || c->err) {
if (reply) cluster_free_reply(reply, 1);
- return FAILURE;
+ return FAILURE;
}
/* Clean up our reply */
diff --git a/redis_session.h b/redis_session.h
index 11f861c2..42ad1e20 100644
--- a/redis_session.h
+++ b/redis_session.h
@@ -3,6 +3,20 @@
#ifdef PHP_SESSION
#include "ext/session/php_session.h"
+typedef struct {
+ zend_bool is_locked;
+ char *session_key;
+ smart_string lock_key;
+ smart_string lock_secret;
+} redis_session_lock_status;
+
+int lock_acquire(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC);
+void lock_release(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC);
+void refresh_lock_status(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC);
+int write_allowed(RedisSock *redis_sock, redis_session_lock_status *lock_status TSRMLS_DC);
+void upload_lock_release_script(RedisSock *redis_sock TSRMLS_DC);
+void calculate_lock_secret(redis_session_lock_status *lock_status);
+
PS_OPEN_FUNC(redis);
PS_CLOSE_FUNC(redis);
PS_READ_FUNC(redis);
@@ -19,4 +33,3 @@ PS_GC_FUNC(rediscluster);
#endif
#endif
-
diff --git a/tests/RedisClusterTest.php b/tests/RedisClusterTest.php
index d03d3591..12e40aa0 100644
--- a/tests/RedisClusterTest.php
+++ b/tests/RedisClusterTest.php
@@ -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
@@ -35,6 +40,21 @@ class Redis_Cluster_Test extends Redis_Test {
public function testMultipleConnect() { return $this->markTestSkipped(); }
public function testDoublePipeNoOp() { 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';
diff --git a/tests/RedisTest.php b/tests/RedisTest.php
index 1a104489..a675092c 100644
--- a/tests/RedisTest.php
+++ b/tests/RedisTest.php
@@ -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();
@@ -5102,15 +5107,175 @@ 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 && $elapsedTime < 3.5);
+ $this->assertTrue($sessionSuccessful);
}
public function testMultipleConnect() {
@@ -5122,5 +5287,76 @@ class Redis_Test extends TestSuite
$this->assertEquals($this->redis->ping(), "+PONG");
}
}
+
+ 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];
+ }
}
?>
diff --git a/tests/getSessionData.php b/tests/getSessionData.php
new file mode 100644
index 00000000..5f993ebc
--- /dev/null
+++ b/tests/getSessionData.php
@@ -0,0 +1,17 @@
+<?php
+$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';
+}
diff --git a/tests/startSession.php b/tests/startSession.php
new file mode 100644
index 00000000..979d966a
--- /dev/null
+++ b/tests/startSession.php
@@ -0,0 +1,36 @@
+<?php
+$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'; \ No newline at end of file