diff --git a/lib/resty/redis/connector.lua b/lib/resty/redis/connector.lua index 06edc7b..46a881b 100644 --- a/lib/resty/redis/connector.lua +++ b/lib/resty/redis/connector.lua @@ -126,6 +126,8 @@ local DEFAULTS = setmetatable({ master_name = "mymaster", role = "master", -- master | slave sentinels = {}, + rotate_slaves = false, + slave_rr_counter = 0, -- Redis proxies typically don't support full Redis capabilities connection_is_proxied = false, @@ -262,6 +264,20 @@ function _M.connect(self, params) end +local function rotate_slaves(self, slaves) + local slaves_length = #slaves + local rotated_slaves = tbl_new(slaves_length, 0) + local local_rr_counter = (self.config.slave_rr_counter + 1) % slaves_length + self.config.slave_rr_counter = local_rr_counter + + for i, _ in ipairs(slaves) do + local_rr_counter = (local_rr_counter % slaves_length) + 1 + rotated_slaves[i] = slaves[local_rr_counter] + end + + return rotated_slaves +end + local function sort_by_localhost(a, b) if a.host == "127.0.0.1" and b.host ~= "127.0.0.1" then return true @@ -319,6 +335,9 @@ function _M.connect_via_sentinel(self, params) sentnl:set_keepalive() + if params.rotate_slaves then + slaves = rotate_slaves(self, slaves) + end -- Put any slaves on 127.0.0.1 at the front tbl_sort(slaves, sort_by_localhost) diff --git a/t/sentinel.t b/t/sentinel.t index 1194b0a..cff4f45 100644 --- a/t/sentinel.t +++ b/t/sentinel.t @@ -147,8 +147,142 @@ location /t { --- no_error_log [error] +=== TEST 4: Slaves rotation is off by default +--- http_config eval: $::HttpConfig +--- config +location /t { + content_by_lua_block { + local rc = require("resty.redis.connector").new({ + url = "sentinel://mymaster:s", + sentinels = { + { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 }, + }, + }) + + for i = 1, 10 do + local sentinel, err = rc:connect() + assert(sentinel and not err, "sentinel should connect without error") + + local result, err = sentinel:get("very_special_key") + assert(result and not err, "get should run without error") + + sentinel:close() + end + + -- Now check slaves metrics for processed commands + -- Only one slave should be used + + local slave_1_was_used = false + + rc = require("resty.redis.connector").new() + + -- SLAVE_1 + local slave_1, err = rc:connect({ + port = $TEST_NGINX_REDIS_PORT_SL1, + }) + assert(slave_1 and not err, "slave_1 should connect without error") + + local metrics, error = slave_1:info("commandstats") + assert(metrics and not error, "info should run without error") + if (metrics:find("cmdstat_get:calls=10")) then + slave_1_was_used = true + else + assert(not metrics:find("cmdstat_get:calls"), "SLAVE_1 " .. metrics) + end + + slave_1:close() + + -- SLAVE_2 + local slave_2, err = rc:connect({ + port = $TEST_NGINX_REDIS_PORT_SL2, + }) + assert(slave_2 and not err, "slave_2 should connect without error") + + local metrics, error = slave_2:info("commandstats") + assert(metrics and not error, "info should run without error") + if (slave_1_was_used) then + assert(not metrics:find("cmdstat_get:calls"), "SLAVE_2 " .. metrics) + else + assert(metrics:find("cmdstat_get:calls=10"), "SLAVE_2 " .. metrics) + end + + slave_2:close() + } +} +--- request +GET /t +--- no_error_log +[error] + +=== TEST 5: When slaves rotation is set slaves should be rotated +--- http_config eval: $::HttpConfig +--- config +location /t { + content_by_lua_block { + local rc = require("resty.redis.connector").new({ + url = "sentinel://mymaster:s", + sentinels = { + { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 }, + }, + rotate_slaves=true, + }) + + for i = 1, 10 do + local sentinel, err = rc:connect() + assert(sentinel and not err, "sentinel should connect without error") + + local result, err = sentinel:get("very_special_key") + assert(result and not err, "get should run without error") + + sentinel:close() + end + + -- Now check slaves metrics for processed commands + -- Both slaves should be used equally + + local slave_1_was_used_in_previous_test = false + + rc = require("resty.redis.connector").new() + + -- SLAVE_1 + local slave_1, err = rc:connect({ + port = $TEST_NGINX_REDIS_PORT_SL1, + }) + assert(slave_1 and not err, "slave_1 should connect without error") + + local metrics, error = slave_1:info("commandstats") + assert(metrics and not error, "info should run without error") + if (metrics:find("cmdstat_get:calls=15")) then + slave_1_was_used_in_previous_test = true + else + assert(metrics:find("cmdstat_get:calls=5"), "SLAVE_1 " .. metrics) + end + + slave_1:close() + + -- SLAVE_2 + local slave_2, err = rc:connect({ + port = $TEST_NGINX_REDIS_PORT_SL2, + }) + assert(slave_2 and not err, "slave_2 should connect without error") + + local metrics, error = slave_2:info("commandstats") + assert(metrics and not error, "info should run without error") + if (slave_1_was_used_in_previous_test) then + assert(metrics:find("cmdstat_get:calls=5"), "SLAVE_2 " .. metrics) + else + assert(metrics:find("cmdstat_get:calls=15"), "SLAVE_2 " .. metrics) + end + + slave_2:close() + } +} +--- request +GET /t +--- no_error_log +[error] -=== TEST 4: Get only healthy slaves +=== TEST 6: Get only healthy slaves --- http_config eval: $::HttpConfig --- config location /t { @@ -210,7 +344,7 @@ GET /t [error] -=== TEST 5: connector.connect_via_sentinel +=== TEST 7: connector.connect_via_sentinel --- http_config eval: $::HttpConfig --- config location /t { @@ -242,7 +376,7 @@ GET /t [error] -=== TEST 6: regression for slave sorting (iss12) +=== TEST 8: regression for slave sorting (iss12) --- http_config eval: $::HttpConfig --- config location /t { @@ -279,7 +413,7 @@ GET /t --- no_error_log [error] -=== TEST 7: connect with acl +=== TEST 9: connect with acl --- http_config eval: $::HttpConfig --- config location /t {