diff --git a/.travis.yml b/.travis.yml index 4078165f..675be1aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,12 +44,15 @@ before_script: - redis-server --port 0 --daemonize yes --unixsocket /tmp/redis.sock - for PORT in $(seq 6379 6382) $(seq 32767 32769); do redis-server --port $PORT --daemonize yes; done - for PORT in $(seq 7000 7011); do redis-server --port $PORT --cluster-enabled yes --cluster-config-file $PORT.conf --daemonize yes; echo 127.0.0.1:$PORT >> tests/nodes/nodemap; done + - for PORT in $(seq 26379 26380); do wget download.redis.io/redis-stable/sentinel.conf -O $PORT.conf; redis-server $PORT.conf --port $PORT --daemonize yes --sentinel; done - echo yes | redis-cli --cluster create $(seq -f 127.0.0.1:%g 7000 7011) --cluster-replicas 3 - echo 'extension = redis.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini script: - php tests/TestRedis.php --class Redis - php tests/TestRedis.php --class RedisArray - php tests/TestRedis.php --class RedisCluster + - php tests/TestRedis.php --class RedisSentinel - USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class Redis - USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisArray - USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisCluster + - USE_ZEND_ALLOC=0 valgrind --error-exitcode=1 php tests/TestRedis.php --class RedisSentinel diff --git a/config.m4 b/config.m4 index bc4dd40d..58b2bce8 100644 --- a/config.m4 +++ b/config.m4 @@ -267,5 +267,5 @@ if test "$PHP_REDIS" != "no"; then dnl dnl PHP_SUBST(REDIS_SHARED_LIBADD) - PHP_NEW_EXTENSION(redis, redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c $lzf_sources, $ext_shared) + PHP_NEW_EXTENSION(redis, redis.c redis_commands.c library.c redis_session.c redis_array.c redis_array_impl.c redis_cluster.c cluster_library.c redis_sentinel.c sentinel_library.c $lzf_sources, $ext_shared) fi diff --git a/library.c b/library.c index d83ffc0f..ebb9fbf4 100644 --- a/library.c +++ b/library.c @@ -167,6 +167,8 @@ redis_error_throw(RedisSock *redis_sock) * Disque) */ if (!REDIS_SOCK_ERRCMP_STATIC(redis_sock, "ERR") && !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOSCRIPT") && + !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOQUORUM") && + !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOGOODSLAVE") && !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "WRONGTYPE") && !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "BUSYGROUP") && !REDIS_SOCK_ERRCMP_STATIC(redis_sock, "NOGROUP")) diff --git a/redis.c b/redis.c index 0b745389..339328f1 100644 --- a/redis.c +++ b/redis.c @@ -22,16 +22,16 @@ #include "config.h" #endif -#include "common.h" -#include "ext/standard/info.h" #include "php_redis.h" -#include "redis_commands.h" #include "redis_array.h" #include "redis_cluster.h" +#include "redis_commands.h" +#include "redis_sentinel.h" #include +#include #ifdef PHP_SESSION -#include "ext/session/php_session.h" +#include #endif #include "library.h" @@ -48,6 +48,7 @@ extern ps_module ps_mod_redis_cluster; extern zend_class_entry *redis_array_ce; extern zend_class_entry *redis_cluster_ce; extern zend_class_entry *redis_cluster_exception_ce; +extern zend_class_entry *redis_sentinel_ce; zend_class_entry *redis_ce; zend_class_entry *redis_exception_ce; @@ -56,6 +57,7 @@ extern int le_cluster_slot_cache; extern zend_function_entry redis_array_functions[]; extern zend_function_entry redis_cluster_functions[]; +extern zend_function_entry redis_sentinel_functions[]; int le_redis_pconnect; @@ -754,6 +756,7 @@ PHP_MINIT_FUNCTION(redis) zend_class_entry redis_class_entry; zend_class_entry redis_array_class_entry; zend_class_entry redis_cluster_class_entry; + zend_class_entry redis_sentinel_class_entry; zend_class_entry redis_exception_class_entry; zend_class_entry redis_cluster_exception_class_entry; @@ -780,6 +783,11 @@ PHP_MINIT_FUNCTION(redis) redis_cluster_ce = zend_register_internal_class(&redis_cluster_class_entry); redis_cluster_ce->create_object = create_cluster_context; + /* RedisSentinel class */ + INIT_CLASS_ENTRY(redis_sentinel_class_entry, "RedisSentinel", redis_sentinel_functions); + redis_sentinel_ce = zend_register_internal_class(&redis_sentinel_class_entry); + redis_sentinel_ce->create_object = create_sentinel_object; + /* Register our cluster cache list item */ le_cluster_slot_cache = zend_register_list_destructors_ex(NULL, cluster_cache_dtor, "Redis cluster slot cache", diff --git a/redis_commands.c b/redis_commands.c index b98d0d27..6945ed4b 100644 --- a/redis_commands.c +++ b/redis_commands.c @@ -3863,6 +3863,30 @@ int redis_xtrim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, return SUCCESS; } +int +redis_sentinel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, void **ctx) +{ + if (zend_parse_parameters(ZEND_NUM_ARGS(), "") == FAILURE) { + return FAILURE; + } + *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SENTINEL", "s", kw, strlen(kw)); + return SUCCESS; +} + +int +redis_sentinel_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, void **ctx) +{ + zend_string *name; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "S", &name) == FAILURE) { + return FAILURE; + } + *cmd_len = REDIS_CMD_SPPRINTF(cmd, "SENTINEL", "sS", kw, strlen(kw), name); + return SUCCESS; +} + /* * Redis commands that don't deal with the server at all. The RedisSock* * pointer is the only thing retrieved differently, so we just take that diff --git a/redis_commands.h b/redis_commands.h index af17d57a..addcce53 100644 --- a/redis_commands.h +++ b/redis_commands.h @@ -289,6 +289,12 @@ int redis_xreadgroup_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, int redis_xtrim_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, char **cmd, int *cmd_len, short *slot, void **ctx); +int redis_sentinel_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, void **ctx); + +int redis_sentinel_str_cmd(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, + char *kw, char **cmd, int *cmd_len, short *slot, void **ctx); + /* Commands that don't communicate with Redis at all (such as getOption, * setOption, _prefix, _serialize, etc). These can be handled in one place * with the method of grabbing our RedisSock* object in different ways diff --git a/redis_sentinel.c b/redis_sentinel.c new file mode 100644 index 00000000..e3590a41 --- /dev/null +++ b/redis_sentinel.c @@ -0,0 +1,94 @@ +#include "php_redis.h" +#include "redis_commands.h" +#include "redis_sentinel.h" + +zend_class_entry *redis_sentinel_ce; + +ZEND_BEGIN_ARG_INFO_EX(arginfo_ctor, 0, 0, 1) + ZEND_ARG_INFO(0, host) + ZEND_ARG_INFO(0, port) +ZEND_END_ARG_INFO() + +zend_function_entry redis_sentinel_functions[] = { + PHP_ME(RedisSentinel, __construct, arginfo_ctor, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, ckquorum, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, failover, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, flushconfig, arginfo_void, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, getMasterAddrByName, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, master, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, masters, arginfo_void, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, ping, arginfo_void, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, reset, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, sentinels, arginfo_value, ZEND_ACC_PUBLIC) + PHP_ME(RedisSentinel, slaves, arginfo_value, ZEND_ACC_PUBLIC) + PHP_FE_END +}; + +PHP_METHOD(RedisSentinel, __construct) +{ + redis_sentinel_object *obj; + zend_long port = -1; + zend_string *host; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "S|l", &host, &port) == FAILURE) { + RETURN_FALSE; + } + + /* If it's not a unix socket, set to default */ + if (port < 0 && ZSTR_LEN(host) > 0 && *ZSTR_VAL(host) != '/') { + port = 26379; + } + + obj = PHPREDIS_GET_OBJECT(redis_sentinel_object, getThis()); + obj->sock = redis_sock_create(ZSTR_VAL(host), ZSTR_LEN(host), port, 0, 0, 0, NULL, 0); +} + +PHP_METHOD(RedisSentinel, ckquorum) +{ + REDIS_PROCESS_KW_CMD("ckquorum", redis_sentinel_str_cmd, redis_boolean_response); +} + +PHP_METHOD(RedisSentinel, failover) +{ + REDIS_PROCESS_KW_CMD("failover", redis_sentinel_str_cmd, redis_boolean_response); +} + +PHP_METHOD(RedisSentinel, flushconfig) +{ + REDIS_PROCESS_KW_CMD("flushconfig", redis_sentinel_cmd, redis_boolean_response); +} + +PHP_METHOD(RedisSentinel, getMasterAddrByName) +{ + REDIS_PROCESS_KW_CMD("get-master-addr-by-name", redis_sentinel_str_cmd, redis_mbulk_reply_raw); +} + +PHP_METHOD(RedisSentinel, master) +{ + REDIS_PROCESS_KW_CMD("master", redis_sentinel_str_cmd, redis_mbulk_reply_zipped_raw); +} + +PHP_METHOD(RedisSentinel, masters) +{ + REDIS_PROCESS_KW_CMD("masters", redis_sentinel_cmd, sentinel_mbulk_reply_zipped_assoc); +} + +PHP_METHOD(RedisSentinel, ping) +{ + REDIS_PROCESS_KW_CMD("PING", redis_empty_cmd, redis_boolean_response); +} + +PHP_METHOD(RedisSentinel, reset) +{ + REDIS_PROCESS_KW_CMD("reset", redis_sentinel_str_cmd, redis_boolean_response); +} + +PHP_METHOD(RedisSentinel, sentinels) +{ + REDIS_PROCESS_KW_CMD("sentinels", redis_sentinel_str_cmd, sentinel_mbulk_reply_zipped_assoc); +} + +PHP_METHOD(RedisSentinel, slaves) +{ + REDIS_PROCESS_KW_CMD("slaves", redis_sentinel_str_cmd, sentinel_mbulk_reply_zipped_assoc); +} diff --git a/redis_sentinel.h b/redis_sentinel.h new file mode 100644 index 00000000..986a63ba --- /dev/null +++ b/redis_sentinel.h @@ -0,0 +1,18 @@ +#ifndef REDIS_SENTINEL_H +#define REDIS_SENTINEL_H + +#include "sentinel_library.h" + +PHP_METHOD(RedisSentinel, __construct); +PHP_METHOD(RedisSentinel, ckquorum); +PHP_METHOD(RedisSentinel, failover); +PHP_METHOD(RedisSentinel, flushconfig); +PHP_METHOD(RedisSentinel, getMasterAddrByName); +PHP_METHOD(RedisSentinel, master); +PHP_METHOD(RedisSentinel, masters); +PHP_METHOD(RedisSentinel, ping); +PHP_METHOD(RedisSentinel, reset); +PHP_METHOD(RedisSentinel, sentinels); +PHP_METHOD(RedisSentinel, slaves); + +#endif /* REDIS_SENTINEL_H */ diff --git a/sentinel_library.c b/sentinel_library.c new file mode 100644 index 00000000..bff5725c --- /dev/null +++ b/sentinel_library.c @@ -0,0 +1,62 @@ +#include "sentinel_library.h" + +static zend_object_handlers redis_sentinel_object_handlers; + +static void +free_redis_sentinel_object(zend_object *object) +{ + redis_sentinel_object *obj = (redis_sentinel_object *)((char *)(object) - XtOffsetOf(redis_sentinel_object, std)); + + if (obj->sock) { + redis_sock_disconnect(obj->sock, 0); + redis_free_socket(obj->sock); + } + zend_object_std_dtor(&obj->std); +} + +zend_object * +create_sentinel_object(zend_class_entry *ce) +{ + redis_sentinel_object *obj = ecalloc(1, sizeof(*obj) + zend_object_properties_size(ce)); + + zend_object_std_init(&obj->std, ce); + object_properties_init(&obj->std, ce); + + memcpy(&redis_sentinel_object_handlers, zend_get_std_object_handlers(), sizeof(redis_sentinel_object_handlers)); + redis_sentinel_object_handlers.offset = XtOffsetOf(redis_sentinel_object, std); + redis_sentinel_object_handlers.free_obj = free_redis_sentinel_object; + obj->std.handlers = &redis_sentinel_object_handlers; + + return &obj->std; +} + +PHP_REDIS_API void +sentinel_mbulk_reply_zipped_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx) +{ + char inbuf[4096]; + int i, nelem; + size_t len; + zval z_ret; + + /* Throws exception on failure */ + if (redis_sock_gets(redis_sock, inbuf, sizeof(inbuf) - 1, &len) < 0) { + RETURN_FALSE; + } + + if (*inbuf != TYPE_MULTIBULK) { + if (*inbuf == TYPE_ERR) { + redis_sock_set_err(redis_sock, inbuf + 1, len - 1); + } + RETURN_FALSE; + } + array_init(&z_ret); + nelem = atoi(inbuf + 1); + for (i = 0; i < nelem; ++i) { + /* redis_mbulk_reply_zipped_raw calls redis_mbulk_reply_zipped + * which puts result into return_value via RETVAL_ZVAL */ + array_init(return_value); + redis_mbulk_reply_zipped_raw(INTERNAL_FUNCTION_PARAM_PASSTHRU, redis_sock, z_tab, ctx); + add_next_index_zval(&z_ret, return_value); + } + RETURN_ZVAL(&z_ret, 0, 1); +} diff --git a/sentinel_library.h b/sentinel_library.h new file mode 100644 index 00000000..460ccfad --- /dev/null +++ b/sentinel_library.h @@ -0,0 +1,13 @@ +#ifndef REDIS_SENTINEL_LIBRARY_H +#define REDIS_SENTINEL_LIBRARY_H + +#include "common.h" +#include "library.h" + +typedef redis_object redis_sentinel_object; + +zend_object *create_sentinel_object(zend_class_entry *ce); + +PHP_REDIS_API void sentinel_mbulk_reply_zipped_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx); + +#endif /* REDIS_SENTINEL_LIBRARY_H */ diff --git a/tests/RedisSentinelTest.php b/tests/RedisSentinelTest.php new file mode 100644 index 00000000..b88e0064 --- /dev/null +++ b/tests/RedisSentinelTest.php @@ -0,0 +1,113 @@ +getHost()); + } + + public function setUp() + { + $this->sentinel = $this->newInstance(); + } + + public function testCkquorum() + { + $this->assertTrue($this->sentinel->ckquorum(self::NAME)); + } + + public function testFailover() + { + $this->assertFalse($this->sentinel->failover(self::NAME)); + } + + public function testFlushconfig() + { + $this->assertTrue($this->sentinel->flushconfig()); + } + + public function testGetMasterAddrByName() + { + $result = $this->sentinel->getMasterAddrByName(self::NAME); + $this->assertTrue(is_array($result)); + $this->assertEquals(2, count($result)); + } + + protected function checkFields(array $fields) + { + foreach ($this->fields as $k) { + $this->assertTrue(array_key_exists($k, $fields)); + } + } + + public function testMaster() + { + $result = $this->sentinel->master(self::NAME); + $this->assertTrue(is_array($result)); + $this->checkFields($result); + } + + public function testMasters() + { + $result = $this->sentinel->masters(); + $this->assertTrue(is_array($result)); + foreach ($result as $master) { + $this->checkFields($master); + } + } + + public function testPing() + { + $this->assertTrue($this->sentinel->ping()); + } + + public function testReset() + { + $this->assertFalse($this->sentinel->reset('*')); + } + + public function testSentinels() + { + $result = $this->sentinel->sentinels(self::NAME); + $this->assertTrue(is_array($result)); + foreach ($result as $sentinel) { + $this->checkFields($sentinel); + } + } + + public function testSlaves() + { + $result = $this->sentinel->slaves(self::NAME); + $this->assertTrue(is_array($result)); + foreach ($result as $slave) { + $this->checkFields($slave); + } + } +} diff --git a/tests/TestRedis.php b/tests/TestRedis.php index 9da9ab86..1c79d702 100644 --- a/tests/TestRedis.php +++ b/tests/TestRedis.php @@ -4,6 +4,7 @@ require_once(dirname($_SERVER['PHP_SELF'])."/TestSuite.php"); require_once(dirname($_SERVER['PHP_SELF'])."/RedisTest.php"); require_once(dirname($_SERVER['PHP_SELF'])."/RedisArrayTest.php"); require_once(dirname($_SERVER['PHP_SELF'])."/RedisClusterTest.php"); +require_once(dirname($_SERVER['PHP_SELF'])."/RedisSentinelTest.php"); /* Make sure errors go to stdout and are shown */ error_reporting(E_ALL); @@ -13,7 +14,7 @@ ini_set( 'display_errors','1'); $arr_args = getopt('', ['host:', 'class:', 'test:', 'nocolors']); /* Grab the test the user is trying to run */ -$arr_valid_classes = ['redis', 'redisarray', 'rediscluster']; +$arr_valid_classes = ['redis', 'redisarray', 'rediscluster', 'redissentinel']; $str_class = isset($arr_args['class']) ? strtolower($arr_args['class']) : 'redis'; $boo_colorize = !isset($arr_args['nocolors']); @@ -25,7 +26,7 @@ $str_host = isset($arr_args['host']) ? $arr_args['host'] : '127.0.0.1'; /* Validate the class is known */ if (!in_array($str_class, $arr_valid_classes)) { - echo "Error: Valid test classes are Redis, RedisArray, and RedisCluster!\n"; + echo "Error: Valid test classes are Redis, RedisArray, RedisCluster and RedisSentinel!\n"; exit(1); } @@ -56,8 +57,11 @@ if ($str_class == 'redis') { } } } -} else { +} else if ($str_class == 'rediscluster') { echo TestSuite::make_bold("RedisCluster") . "\n"; exit(TestSuite::run("Redis_Cluster_Test", $str_filter, $str_host)); +} else { + echo TestSuite::make_bold("RedisSentinel") . "\n"; + exit(TestSuite::run("Redis_Sentinel_Test", $str_filter, $str_host)); } ?>