diff options
author | Marius Meissner <marius.meissner@sixt.com> | 2017-10-30 13:28:33 +0300 |
---|---|---|
committer | Marius Meissner <marius.meissner@sixt.com> | 2017-10-30 13:28:33 +0300 |
commit | abb862d2f50f7f3059d68ef4028685566f865aa7 (patch) | |
tree | a97d507ea4329253c4ab174a6e3e7973e7ab3916 | |
parent | 010336d5a741a74fc43d03df6b3916ba35979db6 (diff) |
PHPREDIS-37: Add locking functionality
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | common.h | 1 | ||||
-rw-r--r-- | php_redis.h | 10 | ||||
-rw-r--r-- | redis.c | 21 | ||||
-rw-r--r-- | redis_session.c | 197 | ||||
-rw-r--r-- | redis_session.h | 15 | ||||
-rw-r--r-- | tests/RedisClusterTest.php | 20 | ||||
-rw-r--r-- | tests/RedisTest.php | 252 | ||||
-rw-r--r-- | tests/getSessionData.php | 17 | ||||
-rw-r--r-- | tests/startSession.php | 36 |
10 files changed, 548 insertions, 22 deletions
@@ -14,3 +14,4 @@ missing autom4te.cache mkinstalldirs run-tests.php +idea/*
\ No newline at end of file @@ -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; @@ -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 |