pax_global_header00006660000000000000000000000064131450657720014524gustar00rootroot0000000000000052 comment=9d56ac3bb568a7e482902e0e8d174167163bf752 lua-resty-redis-connector-0.06/000077500000000000000000000000001314506577200164725ustar00rootroot00000000000000lua-resty-redis-connector-0.06/.gitattributes000066400000000000000000000000321314506577200213600ustar00rootroot00000000000000*.t linguist-language=lua lua-resty-redis-connector-0.06/.gitignore000066400000000000000000000001161314506577200204600ustar00rootroot00000000000000t/servroot/ t/error.log luacov.* *.src.rock lua-resty-redis-connector*.tar.gz lua-resty-redis-connector-0.06/.luacov000066400000000000000000000002161314506577200177630ustar00rootroot00000000000000modules = { ["resty.redis.connector"] = "lib/resty/redis/connector.lua", ["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua", } lua-resty-redis-connector-0.06/Makefile000066400000000000000000000113011314506577200201260ustar00rootroot00000000000000SHELL := /bin/bash # Cheat by using bash :) OPENRESTY_PREFIX = /usr/local/openresty TEST_FILE ?= t SENTINEL_TEST_FILE ?= $(TEST_FILE)/sentinel REDIS_CMD = redis-server SENTINEL_CMD = $(REDIS_CMD) --sentinel REDIS_SOCK = /redis.sock REDIS_PID = /redis.pid REDIS_LOG = /redis.log REDIS_PREFIX = /tmp/redis- # Overrideable redis test variables TEST_REDIS_PORTS ?= 6379 6380 6378 TEST_REDIS_DATABASE ?= 1 REDIS_FIRST_PORT := $(firstword $(TEST_REDIS_PORTS)) REDIS_SLAVE_ARG := --slaveof 127.0.0.1 $(REDIS_FIRST_PORT) REDIS_CLI := redis-cli -p $(REDIS_FIRST_PORT) -n $(TEST_REDIS_DATABASE) # Override socket for running make test on its own # (make test TEST_REDIS_SOCKET=/path/to/sock.sock) TEST_REDIS_SOCKET ?= $(REDIS_PREFIX)$(REDIS_FIRST_PORT)$(REDIS_SOCK) # Overrideable redis + sentinel test variables TEST_SENTINEL_PORTS ?= 6381 6382 6383 TEST_SENTINEL_MASTER_NAME ?= mymaster TEST_SENTINEL_PROMOTION_TIME ?= 20 # Command line arguments for redis tests TEST_REDIS_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ TEST_REDIS_SOCKET=unix://$(TEST_REDIS_SOCKET) \ TEST_REDIS_DATABASE=$(TEST_REDIS_DATABASE) \ TEST_NGINX_NO_SHUFFLE=1 # Command line arguments for sentinel tests TEST_SENTINEL_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ TEST_SENTINEL_PORT=$(firstword $(TEST_SENTINEL_PORTS)) \ TEST_SENTINEL_MASTER_NAME=$(TEST_SENTINEL_MASTER_NAME) \ TEST_REDIS_DATABASE=$(TEST_REDIS_DATABASE) \ TEST_NGINX_NO_SHUFFLE=1 # Sentinel configuration can only be set by a config file define TEST_SENTINEL_CONFIG sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(REDIS_FIRST_PORT) 2 sentinel down-after-milliseconds $(TEST_SENTINEL_MASTER_NAME) 2000 sentinel failover-timeout $(TEST_SENTINEL_MASTER_NAME) 10000 sentinel parallel-syncs $(TEST_SENTINEL_MASTER_NAME) 5 endef export TEST_SENTINEL_CONFIG SENTINEL_CONFIG_FILE = /tmp/sentinel-test-config PREFIX ?= /usr/local LUA_INCLUDE_DIR ?= $(PREFIX)/include LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) PROVE ?= prove -I ../test-nginx/lib INSTALL ?= install .PHONY: all install test test_all start_redis_instances stop_redis_instances \ start_redis_instance stop_redis_instance cleanup_redis_instance flush_db \ create_sentinel_config delete_sentinel_config check_ports test_redis \ test_sentinel sleep all: ; install: all $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis $(INSTALL) lib/resty/redis/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis test: test_redis test_all: start_redis_instances sleep test_redis stop_redis_instances sleep: sleep 3 start_redis_instances: check_ports create_sentinel_config @$(foreach port,$(TEST_REDIS_PORTS), \ [[ "$(port)" != "$(REDIS_FIRST_PORT)" ]] && \ SLAVE="$(REDIS_SLAVE_ARG)" || \ SLAVE="" && \ $(MAKE) start_redis_instance args="$$SLAVE" port=$(port) \ prefix=$(REDIS_PREFIX)$(port) && \ ) true @$(foreach port,$(TEST_SENTINEL_PORTS), \ $(MAKE) start_redis_instance \ port=$(port) args="$(SENTINEL_CONFIG_FILE) --sentinel" \ prefix=$(REDIS_PREFIX)$(port) && \ ) true stop_redis_instances: delete_sentinel_config -@$(foreach port,$(TEST_REDIS_PORTS) $(TEST_SENTINEL_PORTS), \ $(MAKE) stop_redis_instance cleanup_redis_instance port=$(port) \ prefix=$(REDIS_PREFIX)$(port) && \ ) true 2>&1 > /dev/null start_redis_instance: -@echo "Starting redis on port $(port) with args: \"$(args)\"" -@mkdir -p $(prefix) @$(REDIS_CMD) $(args) \ --pidfile $(prefix)$(REDIS_PID) \ --bind 127.0.0.1 --port $(port) \ --unixsocket $(prefix)$(REDIS_SOCK) \ --unixsocketperm 777 \ --dir $(prefix) \ --logfile $(prefix)$(REDIS_LOG) \ --loglevel debug \ --daemonize yes stop_redis_instance: -@echo "Stopping redis on port $(port)" -@[[ -f "$(prefix)$(REDIS_PID)" ]] && kill -QUIT \ `cat $(prefix)$(REDIS_PID)` 2>&1 > /dev/null || true cleanup_redis_instance: stop_redis_instance -@echo "Cleaning up redis files in $(prefix)" -@rm -rf $(prefix) flush_db: -@echo "Flushing Redis DB" @$(REDIS_CLI) flushdb create_sentinel_config: -@echo "Creating $(SENTINEL_CONFIG_FILE)" @echo "$$TEST_SENTINEL_CONFIG" > $(SENTINEL_CONFIG_FILE) delete_sentinel_config: -@echo "Removing $(SENTINEL_CONFIG_FILE)" @rm -f $(SENTINEL_CONFIG_FILE) check_ports: -@echo "Checking ports $(REDIS_PORTS)" @$(foreach port,$(REDIS_PORTS),! lsof -i :$(port) &&) true 2>&1 > /dev/null test_redis: flush_db util/lua-releng @rm -f luacov.stats.out $(TEST_REDIS_VARS) $(PROVE) $(TEST_FILE) @luacov @tail -7 luacov.report.out test_leak: flush_db $(TEST_REDIS_VARS) TEST_NGINX_CHECK_LEAK=1 $(PROVE) $(TEST_FILE) lua-resty-redis-connector-0.06/README.md000066400000000000000000000171111314506577200177520ustar00rootroot00000000000000# lua-resty-redis-connector Connection utilities for [lua-resty-redis](https://github.com/openresty/lua-resty-redis), making it easy and reliable to connect to Redis hosts, either directly or via [Redis Sentinel](http://redis.io/topics/sentinel). ## Synopsis Quick and simple authenticated connection on localhost to DB 2: ```lua local redis, err = require("resty.redis.connector").new({ url = "redis://PASSWORD@127.0.0.1:6379/2", }):connect() ``` More verbose configuration, with timeouts and a default password: ```lua local rc = require("resty.redis.connector").new({ connect_timeout = 50, read_timeout = 5000, keepalive_timeout = 30000, password = "mypass", }) local redis, err = rc:connect({ url = "redis://127.0.0.1:6379/2", }) -- ... local ok, err = rc:set_keepalive(redis) -- uses keepalive params ``` Keep all config in a table, to easily create / close connections as needed: ```lua local rc = require("resty.redis.connector").new({ connect_timeout = 50, read_timeout = 5000, keepalive_timeout = 30000, host = "127.0.0.1", port = 6379, db = 2, password = "mypass", }) local redis, err = rc:connect() -- ... local ok, err = rc:set_keepalive(redis) ``` [connect](#connect) can be used to override some defaults given in [new](#new), which are pertinent to this connection only. ```lua local rc = require("resty.redis.connector").new({ host = "127.0.0.1", port = 6379, db = 2, }) local redis, err = rc:connect({ db = 5, }) ``` ## DSN format If the `params.url` field is present then it will be parsed to set the other params. Any manually specified params will override values given in the DSN. *Note: this is a behaviour change as of v0.06. Previously, the DSN values would take precedence.* ### Direct Redis connections The format for connecting directly to Redis is: `redis://PASSWORD@HOST:PORT/DB` The `PASSWORD` and `DB` fields are optional, all other components are required. ### Connections via Redis Sentinel When connecting via Redis Sentinel, the format is as follows: `sentinel://PASSWORD@MASTER_NAME:ROLE/DB` Again, `PASSWORD` and `DB` are optional. `ROLE` must be either `m` or `s` for master / slave respectively. A table of `sentinels` must also be supplied. e.g. ```lua local redis, err = rc:connect{ url = "sentinel://mymaster:a/2", sentinels = { { host = "127.0.0.1", port = 26379" }, } } ``` ## Proxy Mode Enable the `connection_is_proxied` parameter if connecting to Redis through a proxy service (e.g. Twemproxy). These proxies generally only support a limited sub-set of Redis commands, those which do not require state and do not affect multiple keys. Databases and transactions are also not supported. Proxy mode will disable switching to a DB on connect. Unsupported commands (defaults to those not supported by Twemproxy) will return `nil, err` immediately rather than being sent to the proxy, which can result in dropped connections. `discard` will not be sent when adding connections to the keepalive pool ## Disabled commands If configured as a table of commands, the command methods will be replaced by a function which immediately returns `nil, err` without forwarding the command to the server ## Default Parameters ```lua { connect_timeout = 100, read_timeout = 1000, connection_options = {}, -- pool, etc keepalive_timeout = 60000, keepalive_poolsize = 30, host = "127.0.0.1", port = "6379", path = "", -- unix socket path, e.g. /tmp/redis.sock password = "", db = 0, master_name = "mymaster", role = "master", -- master | slave sentinels = {}, connection_is_proxied = false, disabled_commands = {}, } ``` ## API * [new](#new) * [connect](#connect) * [set_keepalive](#set_keepalive) * [Utilities](#utilities) * [connect_via_sentinel](#connect_via_sentinel) * [try_hosts](#try_hosts) * [connect_to_host](#connect_to_host) * [sentinel.get_master](#sentinelget_master) * [sentinel.get_slaves](#sentinelget_slaves) ### new `syntax: rc = redis_connector.new(params)` Creates the Redis Connector object, overring default params with the ones given. In case of failures, returns `nil` and a string describing the error. ### connect `syntax: redis, err = rc:connect(params)` Attempts to create a connection, according to the [params](#parameters) supplied, falling back to defaults given in `new` or the predefined defaults. If a connection cannot be made, returns `nil` and a string describing the reason. Note that `params` given here do not change the connector's own configuration, and are only used to alter this particular connection operation. As such, the following parameters have no meaning when given in `connect`. * `keepalive_poolsize` * `keepalive_timeout` * `connection_is_proxied` * `disabled_commands` ### set_keepalive `syntax: ok, err = rc:set_keepalive(redis)` Attempts to place the given Redis connection on the keepalive pool, according to timeout and poolsize params given in `new` or the predefined defaults. This allows an application to release resources without having to keep track of application wide keepalive settings. Returns `1` or in the case of error, `nil` and a string describing the error. ## Utilities The following methods are not typically needed, but may be useful if a custom interface is required. ### connect_via_sentinel `syntax: redis, err = rc:connect_via_sentinel(params)` Returns a Redis connection by first accessing a sentinel as supplied by the `params.sentinels` table, and querying this with the `params.master_name` and `params.role`. ### try_hosts `syntax: redis, err = rc:try_hosts(hosts)` Tries the hosts supplied in order and returns the first successful connection. ### connect_to_host `syntax: redis, err = rc:connect_to_host(host)` Attempts to connect to the supplied `host`. ### sentinel.get_master `syntax: master, err = sentinel.get_master(sentinel, master_name)` Given a connected Sentinel instance and a master name, will return the current master Redis instance. ### sentinel.get_slaves `syntax: slaves, err = sentinel.get_slaves(sentinel, master_name)` Given a connected Sentinel instance and a master name, will return a list of registered slave Redis instances. # Author James Hurst # Licence This module is licensed under the 2-clause BSD license. Copyright (c) James Hurst All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. lua-resty-redis-connector-0.06/dist.ini000066400000000000000000000005431314506577200201400ustar00rootroot00000000000000name=lua-resty-redis-connector abstract=Connection utilities for lua-resty-redis, making it easy and reliable to connect to Redis hosts, either directly or via Redis Sentinel. author=James Hurst is_original=yes license=2bsd lib_dir=lib doc_dir=lib repo_link=https://github.com/pintsized/lua-resty-redis-connector main_module=lib/resty/redis/connector.lua lua-resty-redis-connector-0.06/lib/000077500000000000000000000000001314506577200172405ustar00rootroot00000000000000lua-resty-redis-connector-0.06/lib/resty/000077500000000000000000000000001314506577200204065ustar00rootroot00000000000000lua-resty-redis-connector-0.06/lib/resty/redis/000077500000000000000000000000001314506577200215145ustar00rootroot00000000000000lua-resty-redis-connector-0.06/lib/resty/redis/connector.lua000066400000000000000000000246741314506577200242260ustar00rootroot00000000000000local ipairs, pcall, error, tostring, type, next, setmetatable, getmetatable = ipairs, pcall, error, tostring, type, next, setmetatable, getmetatable local ngx_log = ngx.log local ngx_ERR = ngx.ERR local ngx_re_match = ngx.re.match local tbl_remove = table.remove local tbl_sort = table.sort local ok, tbl_new = pcall(require, "table.new") if not ok then tbl_new = function (narr, nrec) return {} end end local redis = require("resty.redis") redis.add_commands("sentinel") local get_master = require("resty.redis.sentinel").get_master local get_slaves = require("resty.redis.sentinel").get_slaves -- A metatable which prevents undefined fields from being created / accessed local fixed_field_metatable = { __index = function(t, k) error("field " .. tostring(k) .. " does not exist", 3) end, __newindex = function(t, k, v) error("attempt to create new field " .. tostring(k), 3) end, } -- Returns a new table, recursively copied from the one given, retaining -- metatable assignment. -- -- @param table table to be copied -- @return table local function tbl_copy(orig) local orig_type = type(orig) local copy if orig_type == "table" then copy = {} for orig_key, orig_value in next, orig, nil do copy[tbl_copy(orig_key)] = tbl_copy(orig_value) end setmetatable(copy, tbl_copy(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end -- Returns a new table, recursively copied from the combination of the given -- table `t1`, with any missing fields copied from `defaults`. -- -- If `defaults` is of type "fixed field" and `t1` contains a field name not -- present in the defults, an error will be thrown. -- -- @param table t1 -- @param table defaults -- @return table a new table, recursively copied and merged local function tbl_copy_merge_defaults(t1, defaults) if t1 == nil then t1 = {} end if defaults == nil then defaults = {} end if type(t1) == "table" and type(defaults) == "table" then local mt = getmetatable(defaults) local copy = {} for t1_key, t1_value in next, t1, nil do copy[tbl_copy(t1_key)] = tbl_copy_merge_defaults( t1_value, tbl_copy(defaults[t1_key]) ) end for defaults_key, defaults_value in next, defaults, nil do if t1[defaults_key] == nil then copy[tbl_copy(defaults_key)] = tbl_copy(defaults_value) end end return copy else return t1 -- not a table end end local DEFAULTS = setmetatable({ connect_timeout = 100, read_timeout = 1000, connection_options = {}, -- pool, etc keepalive_timeout = 60000, keepalive_poolsize = 30, host = "127.0.0.1", port = 6379, path = "", -- /tmp/redis.sock password = "", db = 0, url = "", -- DSN url master_name = "mymaster", role = "master", -- master | slave sentinels = {}, -- Redis proxies typically don't support full Redis capabilities connection_is_proxied = false, disabled_commands = {}, }, fixed_field_metatable) -- This is the set of commands unsupported by Twemproxy local default_disabled_commands = { "migrate", "move", "object", "randomkey", "rename", "renamenx", "scan", "bitop", "msetnx", "blpop", "brpop", "brpoplpush", "psubscribe", "publish", "punsubscribe", "subscribe", "unsubscribe", "discard", "exec", "multi", "unwatch", "watch", "script", "auth", "echo", "select", "bgrewriteaof", "bgsave", "client", "config", "dbsize", "debug", "flushall", "flushdb", "info", "lastsave", "monitor", "save", "shutdown", "slaveof", "slowlog", "sync", "time" } local _M = { _VERSION = '0.06', } local mt = { __index = _M } local function parse_dsn(params) local url = params.url if url and url ~= "" then local url_pattern = [[^(?:(redis|sentinel)://)(?:([^@]*)@)?([^:/]+)(?::(\d+|[msa]+))/?(.*)$]] local m, err = ngx_re_match(url, url_pattern, "oj") if not m then return nil, "could not parse DSN: " .. tostring(err) end -- TODO: Support a 'protocol' for proxied Redis? local fields if m[1] == "redis" then fields = { "password", "host", "port", "db" } elseif m[1] == "sentinel" then fields = { "password", "master_name", "role", "db" } end -- password may not be present if #m < 5 then tbl_remove(fields, 1) end local roles = { m = "master", s = "slave" } local parsed_params = {} for i,v in ipairs(fields) do parsed_params[v] = m[i + 1] if v == "role" then parsed_params[v] = roles[parsed_params[v]] end end return tbl_copy_merge_defaults(params, parsed_params) end end _M.parse_dsn = parse_dsn function _M.new(config) -- Fill out gaps in config with any dsn params if config and config.url then local err config, err = parse_dsn(config) if not ok then ngx_log(ngx_ERR, err) end end local ok, config = pcall(tbl_copy_merge_defaults, config, DEFAULTS) if not ok then return nil, config -- err else -- In proxied Redis mode disable default commands if config.connection_is_proxied == true and not next(config.disabled_commands) then config.disabled_commands = default_disabled_commands end return setmetatable({ config = setmetatable(config, fixed_field_metatable) }, mt) end end function _M.connect(self, params) if params and params.url then local err params, err = parse_dsn(params) if not ok then ngx_log(ngx_ERR, err) end end params = tbl_copy_merge_defaults(params, self.config) if #params.sentinels > 0 then return self:connect_via_sentinel(params) else return self:connect_to_host(params) end 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 else return false end end function _M.connect_via_sentinel(self, params) local sentinels = params.sentinels local master_name = params.master_name local role = params.role local db = params.db local password = params.password local sentnl, err, previous_errors = self:try_hosts(sentinels) if not sentnl then return nil, err, previous_errors end if role == "master" then local master, err = get_master(sentnl, master_name) if master then master.db = db master.password = password local redis, err = self:connect_to_host(master) if redis then sentnl:set_keepalive() return redis, err else if role == "master" then return nil, err end end end else -- We want a slave local slaves, err = get_slaves(sentnl, master_name) sentnl:set_keepalive() if not slaves then return nil, err end -- Put any slaves on 127.0.0.1 at the front tbl_sort(slaves, sort_by_localhost) if db or password then for i,slave in ipairs(slaves) do slave.db = db slave.password = password end end local slave, err, previous_errors = self:try_hosts(slaves) if not slave then return nil, err, previous_errors else return slave end end end -- In case of errors, returns "nil, err, previous_errors" where err is -- the last error received, and previous_errors is a table of the previous errors. function _M.try_hosts(self, hosts) local errors = tbl_new(#hosts, 0) for i, host in ipairs(hosts) do local r, err = self:connect_to_host(host) if r and not err then return r, nil, errors else errors[i] = err end end return nil, "no hosts available", errors end function _M.connect_to_host(self, host) local r = redis.new() local config = self.config r:set_timeout(config.connect_timeout) -- Stub out methods for disabled commands if next(config.disabled_commands) then for _, cmd in ipairs(config.disabled_commands) do r[cmd] = function(...) return nil, ("Command "..cmd.." is disabled") end end end local ok, err local path = host.path local opts = config.connection_options if path and path ~= "" then if opts then ok, err = r:connect(path, config.connection_options) else ok, err = r:connect(path) end else if opts then ok, err = r:connect(host.host, host.port, config.connection_options) else ok, err = r:connect(host.host, host.port) end end if not ok then return nil, err else r:set_timeout(self, config.read_timeout) local password = host.password if password and password ~= "" then local res, err = r:auth(password) if err then ngx_log(ngx_ERR, err) return res, err end end -- No support for DBs in proxied Redis. if config.connection_is_proxied ~= true and host.db ~= nil then r:select(host.db) end return r, nil end end local function set_keepalive(self, redis) -- Restore connection to "NORMAL" before putting into keepalive pool, -- ignoring any errors. -- Proxied Redis does not support transactions. if self.config.connection_is_proxied ~= true then redis:discard() end local config = self.config return redis:set_keepalive( config.keepalive_timeout, config.keepalive_poolsize ) end _M.set_keepalive = set_keepalive -- Deprecated: use config table in new() or connect() instead. function _M.set_connect_timeout(self, timeout) self.config.connect_timeout = timeout end -- Deprecated: use config table in new() or connect() instead. function _M.set_read_timeout(self, timeout) self.config.read_timeout = timeout end -- Deprecated: use config table in new() or connect() instead. function _M.set_connection_options(self, options) self.config.connection_options = options end return setmetatable(_M, fixed_field_metatable) lua-resty-redis-connector-0.06/lib/resty/redis/sentinel.lua000066400000000000000000000025011314506577200240360ustar00rootroot00000000000000local ipairs, type = ipairs, type local ngx_null = ngx.null local tbl_insert = table.insert local ok, tbl_new = pcall(require, "table.new") if not ok then tbl_new = function (narr, nrec) return {} end end local _M = { _VERSION = '0.06' } function _M.get_master(sentinel, master_name) local res, err = sentinel:sentinel( "get-master-addr-by-name", master_name ) if res and res ~= ngx_null and res[1] and res[2] then return { host = res[1], port = res[2] } else return nil, err end end function _M.get_slaves(sentinel, master_name) local res, err = sentinel:sentinel("slaves", master_name) if res and type(res) == "table" then local hosts = tbl_new(#res, 0) for _,slave in ipairs(res) do local num_recs = #slave local host = tbl_new(0, num_recs + 1) for i = 1, num_recs, 2 do host[slave[i]] = slave[i + 1] end if host["master-link-status"] == "ok" then host.host = host.ip -- for parity with other functions tbl_insert(hosts, host) end end if hosts[1] ~= nil then return hosts else return nil, "no slaves available" end else return nil, err end end return _M lua-resty-redis-connector-0.06/lua-resty-redis-connector-0.06-0.rockspec000066400000000000000000000013671314506577200257730ustar00rootroot00000000000000package = "lua-resty-redis-connector" version = "0.06-0" source = { url = "git://github.com/pintsized/lua-resty-redis-connector", tag = "v0.06" } description = { summary = "Connection utilities for lua-resty-redis.", detailed = [[ Connection utilities for lua-resty-redis, making it easy and reliable to connect to Redis hosts, either directly or via Redis Sentinel. ]], homepage = "https://github.com/pintsized/lua-resty-redis-connector", license = "2-clause BSD", maintainer = "James Hurst " } dependencies = { "lua >= 5.1", } build = { type = "builtin", modules = { ["resty.redis.connector"] = "lib/resty/redis/connector.lua", ["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua" } } lua-resty-redis-connector-0.06/t/000077500000000000000000000000001314506577200167355ustar00rootroot00000000000000lua-resty-redis-connector-0.06/t/config.t000066400000000000000000000077541314506577200204040ustar00rootroot00000000000000use Test::Nginx::Socket::Lua; use Cwd qw(cwd); repeat_each(2); plan tests => repeat_each() * blocks() * 2; my $pwd = cwd(); our $HttpConfig = qq{ lua_package_path "$pwd/lib/?.lua;;"; init_by_lua_block { require("luacov.runner").init() } }; $ENV{TEST_NGINX_REDIS_PORT} ||= 6379; no_long_string(); run_tests(); __DATA__ === TEST 1: Default config --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local redis = assert(rc:connect(), "rc:connect should return postively") assert(redis:set("foo", "bar"), "redis:set should return positvely") assert(redis:get("foo") == "bar", "get(foo) should return bar") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 2: Defaults via new --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local config = { connect_timeout = 500, port = 6380, db = 6, } local rc = require("resty.redis.connector").new(config) assert(config ~= rc.config, "config should not equal rc.config") assert(rc.config.connect_timeout == 500, "connect_timeout should be 500") assert(rc.config.db == 6, "db should be 6") assert(rc.config.role == "master", "role should be master") } } --- request GET /t --- no_error_log [error] === TEST 3: Config via connect still overrides --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ connect_timeout = 500, port = 6380, db = 6, keepalive_poolsize = 10, }) assert(config ~= rc.config, "config should not equal rc.config") assert(rc.config.connect_timeout == 500, "connect_timeout should be 500") assert(rc.config.db == 6, "db should be 6") assert(rc.config.role == "master", "role should be master") assert(rc.config.keepalive_poolsize == 10, "keepalive_poolsize should be 10") local redis = assert(rc:connect({ port = 6379 }), "rc:connect should return positively") } } --- request GET /t --- no_error_log [error] === TEST 4: Unknown config errors, all config does not error --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc, err = require("resty.redis.connector").new({ connect_timeout = 500, port = 6380, db = 6, foo = "bar", }) assert(rc == nil, "rc should be nil") assert(string.find(err, "field foo does not exist"), "err should contain error") -- Provide all options, without errors assert(require("resty.redis.connector").new({ connect_timeout = 100, read_timeout = 1000, connection_options = { pool = "::" }, keepalive_timeout = 60000, keepalive_poolsize = 30, host = "127.0.0.1", port = 6379, path = "", password = "", db = 0, url = "", master_name = "mymaster", role = "master", sentinels = {}, }), "new should return positively") -- Provide all options via connect, without errors local rc = require("resty.redis.connector").new() assert(rc:connect({ connect_timeout = 100, read_timeout = 1000, connection_options = { pool = "::" }, keepalive_timeout = 60000, keepalive_poolsize = 30, host = "127.0.0.1", port = 6379, path = "", password = "", db = 0, url = "", master_name = "mymaster", role = "master", sentinels = {}, }), "rc:connect should return positively") } } --- request GET /t --- no_error_log [error] lua-resty-redis-connector-0.06/t/connector.t000066400000000000000000000225041314506577200211170ustar00rootroot00000000000000use Test::Nginx::Socket 'no_plan'; use Cwd qw(cwd); my $pwd = cwd(); our $HttpConfig = qq{ lua_package_path "$pwd/lib/?.lua;;"; init_by_lua_block { require("luacov.runner").init() } }; $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; $ENV{TEST_NGINX_REDIS_PORT} ||= 6379; no_long_string(); run_tests(); __DATA__ === TEST 1: basic connect --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT }) local redis, err = assert(rc:connect(params), "connect should return positively") assert(redis:set("dog", "an animal"), "redis:set should return positively") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 2: try_hosts --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors off; content_by_lua_block { local rc = require("resty.redis.connector").new({ connect_timeout = 100, }) local hosts = { { host = "127.0.0.1", port = 1 }, { host = "127.0.0.1", port = 2 }, { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT }, } local redis, err, previous_errors = rc:try_hosts(hosts) assert(redis and not err, "try_hosts should return a connection and no error") assert(previous_errors[1] == "connection refused", "previous_errors[1] should be 'connection refused'") assert(previous_errors[2] == "connection refused", "previous_errors[2] should be 'connection refused'") assert(redis:set("dog", "an animal"), "redis connection should be working") redis:close() local hosts = { { host = "127.0.0.1", port = 1 }, { host = "127.0.0.1", port = 2 }, } local redis, err, previous_errors = rc:try_hosts(hosts) assert(not redis and err == "no hosts available", "no available hosts should return an error") assert(previous_errors[1] == "connection refused", "previous_errors[1] should be 'connection refused'") assert(previous_errors[2] == "connection refused", "previous_errors[2] should be 'connection refused'") } } --- request GET /t --- no_error_log [error] === TEST 3: connect_to_host --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local host = { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT } local redis, err = rc:connect_to_host(host) assert(redis and not err, "connect_to_host should return positively") assert(redis:set("dog", "an animal"), "redis connection should be working") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 4: connect_to_host options ignore defaults --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, db = 2, }) local redis, err = assert(rc:connect_to_host({ host = "127.0.0.1", db = 1, port = $TEST_NGINX_REDIS_PORT }), "connect_to_host should return positively") assert(redis:set("dog", "an animal") == "OK", "set should return 'OK'") redis:select(2) assert(redis:get("dog") == ngx.null, "dog should not exist in db 2") redis:select(1) assert(redis:get("dog") == "an animal", "dog should be 'an animal' in db 1") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 5: Test set_keepalive method --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, }) local redis = assert(rc:connect(), "rc:connect should return positively") local ok, err = rc:set_keepalive(redis) assert(not err, "set_keepalive error should be nil") local ok, err = redis:set("foo", "bar") assert(not ok, "ok should be nil") assert(string.find(err, "closed"), "error should contain 'closed'") local redis = assert(rc:connect(), "connect should return positively") assert(redis:subscribe("channel"), "subscribe should return positively") local ok, err = rc:set_keepalive(redis) assert(not ok, "ok should be nil") assert(string.find(err, "subscribed state"), "error should contain 'subscribed state'") } } --- request GET /t --- no_error_log [error] === TEST 6: password --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, password = "foo", }) local redis, err = rc:connect() assert(not redis and string.find(err, "ERR Client sent AUTH, but no password is set"), "connect should fail with password error") } } --- request GET /t --- error_log ERR Client sent AUTH, but no password is set === TEST 7: unix domain socket --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local redis, err = require("resty.redis.connector").new({ path = "unix://tmp/redis.sock", }):connect() assert(not redis and err == "no such file or directory", "bad domain socket should fail") } } --- request GET /t --- no_error_log [error] === TEST 8: parse_dsn --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local rc = require("resty.redis.connector") local user_params = { url = "redis://foo@127.0.0.1:$TEST_NGINX_REDIS_PORT/4" } local params, err = rc.parse_dsn(user_params) assert(params and not err, "url should parse without error: " .. tostring(err)) assert(params.host == "127.0.0.1", "host should be localhost") assert(tonumber(params.port) == $TEST_NGINX_REDIS_PORT, "port should be $TEST_NGINX_REDIS_PORT") assert(tonumber(params.db) == 4, "db should be 4") assert(params.password == "foo", "password should be foo") local user_params = { url = "sentinel://foo@foomaster:s/2" } local params, err = rc.parse_dsn(user_params) assert(params and not err, "url should parse without error: " .. tostring(err)) assert(params.master_name == "foomaster", "master_name should be foomaster") assert(params.role == "slave", "role should be slave") assert(tonumber(params.db) == 2, "db should be 2") local params = { url = "sentinels:/wrongformat", } local ok, err = rc.parse_dsn(params) assert(not ok and err == "could not parse DSN: nil", "url should fail to parse") } } --- request GET /t --- no_error_log [error] === TEST 9: params override dsn components --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local rc = require("resty.redis.connector") local user_params = { url = "redis://foo@127.0.0.1:6381/4", db = 2, password = "bar", host = "example.com", } local params, err = rc.parse_dsn(user_params) assert(params and not err, "url should parse without error: " .. tostring(err)) assert(tonumber(params.db) == 2, "db should be 2") assert(params.password == "bar", "password should be bar") assert(params.host == "example.com", "host should be example.com") assert(tonumber(params.port) == 6381, "ort should still be 6381") } } --- request GET /t --- no_error_log [error] === TEST 9: Integration test for parse_dsn --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local user_params = { url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", db = 2, host = "127.0.0.1", } local rc, err = require("resty.redis.connector").new(user_params) assert(rc and not err, "new should return positively") local redis, err = rc:connect() assert(redis and not err, "connect should return positively") assert(redis:set("cat", "dog") and redis:get("cat") == "dog") local redis, err = rc:connect({ url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", db = 2, host = "127.0.0.1", }) assert(redis and not err, "connect should return positively") assert(redis:set("cat", "dog") and redis:get("cat") == "dog") local rc2, err = require("resty.redis.connector").new() local redis, err = rc2:connect({ url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", db = 2, host = "127.0.0.1", }) assert(redis and not err, "connect should return positively") assert(redis:set("cat", "dog") and redis:get("cat") == "dog") } } --- request GET /t --- no_error_log [error] lua-resty-redis-connector-0.06/t/proxy.t000066400000000000000000000073711314506577200203130ustar00rootroot00000000000000use Test::Nginx::Socket 'no_plan'; use Cwd qw(cwd); my $pwd = cwd(); our $HttpConfig = qq{ lua_package_path "$pwd/lib/?.lua;;"; init_by_lua_block { require("luacov.runner").init() } }; $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; $ENV{TEST_NGINX_REDIS_PORT} ||= 6379; no_long_string(); run_tests(); __DATA__ === TEST 1: Proxy mode disables commands --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, connection_is_proxied = true }) local redis, err = assert(rc:connect(params), "connect should return positively") assert(redis:set("dog", "an animal"), "redis:set should return positively") local ok, err = redis:multi() assert(ok == nil, "redis:multi should return nil") assert(err == "Command multi is disabled") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 2: Proxy mode disables custom commands --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, connection_is_proxied = true, disabled_commands = { "foobar", "hget"} }) local redis, err = assert(rc:connect(params), "connect should return positively") assert(redis:set("dog", "an animal"), "redis:set should return positively") assert(redis:multi(), "redis:multi should return positively") local ok, err = redis:hget() assert(ok == nil, "redis:hget should return nil") assert(err == "Command hget is disabled") local ok, err = redis:foobar() assert(ok == nil, "redis:foobar should return nil") assert(err == "Command foobar is disabled") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 3: Proxy mode does not switch DB --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local redis = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, db = 2 }):connect() local proxy = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, connection_is_proxied = true, db = 2 }):connect() assert(redis:set("proxy", "test"), "redis:set should return positively") assert(proxy:get("proxy") == ngx.null, "proxy key should not exist in proxy") redis:seelct(2) assert(redis:get("proxy") == "test", "proxy key should be 'test' in db 1") redis:close() } } --- request GET /t --- no_error_log [error] === TEST 4: Commands are disabled without proxy mode --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new({ port = $TEST_NGINX_REDIS_PORT, disabled_commands = { "foobar", "hget"} }) local redis, err = assert(rc:connect(params), "connect should return positively") assert(redis:set("dog", "an animal"), "redis:set should return positively") assert(redis:multi(), "redis:multi should return positively") local ok, err = redis:hget() assert(ok == nil, "redis:hget should return nil") assert(err == "Command hget is disabled") local ok, err = redis:foobar() assert(ok == nil, "redis:foobar should return nil") assert(err == "Command foobar is disabled") redis:close() } } --- request GET /t --- no_error_log [error] lua-resty-redis-connector-0.06/t/sentinel.t000066400000000000000000000135021314506577200207440ustar00rootroot00000000000000use Test::Nginx::Socket 'no_plan'; use Cwd qw(cwd); my $pwd = cwd(); our $HttpConfig = qq{ lua_package_path "$pwd/lib/?.lua;;"; init_by_lua_block { require("luacov.runner").init() } }; $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; $ENV{TEST_NGINX_REDIS_PORT} ||= 6379; no_long_string(); run_tests(); __DATA__ === TEST 1: Get the master --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local sentinel, err = rc:connect{ url = "redis://127.0.0.1:6381" } assert(sentinel and not err, "sentinel should connect without errors") local master, err = require("resty.redis.sentinel").get_master( sentinel, "mymaster" ) assert(master and not err, "get_master should return the master") assert(master.host == "127.0.0.1" and tonumber(master.port) == 6379, "host should be 127.0.0.1 and port should be 6379") sentinel:close() } } --- request GET /t --- no_error_log [error] === TEST 1b: Get the master directly --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local master, err = rc:connect({ url = "sentinel://mymaster:m/3", sentinels = { { host = "127.0.0.1", port = 6381 } } }) assert(master and not err, "get_master should return the master") assert(master:set("foo", "bar"), "set should run without error") assert(master:get("foo") == "bar", "get(foo) should return bar") master:close() } } --- request GET /t --- no_error_log [error] === TEST 2: Get slaves --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local sentinel, err = rc:connect{ url = "redis://127.0.0.1:6381" } assert(sentinel and not err, "sentinel should connect without error") local slaves, err = require("resty.redis.sentinel").get_slaves( sentinel, "mymaster" ) assert(slaves and not err, "slaves should be returned without error") local slaveports = { ["6378"] = false, ["6380"] = false } for _,slave in ipairs(slaves) do slaveports[tostring(slave.port)] = true end assert(slaveports["6378"] == true and slaveports["6380"] == true, "slaves should both be found") sentinel:close() } } --- request GET /t --- no_error_log [error] === TEST 3: Get only healthy slaves --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local sentinel, err = rc:connect({ url = "redis://127.0.0.1:6381" }) assert(sentinel and not err, "sentinel should connect without error") local slaves, err = require("resty.redis.sentinel").get_slaves( sentinel, "mymaster" ) assert(slaves and not err, "slaves should be returned without error") local slaveports = { ["6378"] = false, ["6380"] = false } for _,slave in ipairs(slaves) do slaveports[tostring(slave.port)] = true end assert(slaveports["6378"] == true and slaveports["6380"] == true, "slaves should both be found") -- connect to one and remove it local r = require("resty.redis.connector").new():connect({ port = 6378, }) r:slaveof("127.0.0.1", 7000) ngx.sleep(9) local slaves, err = require("resty.redis.sentinel").get_slaves( sentinel, "mymaster" ) assert(slaves and not err, "slaves should be returned without error") local slaveports = { ["6378"] = false, ["6380"] = false } for _,slave in ipairs(slaves) do slaveports[tostring(slave.port)] = true end assert(slaveports["6378"] == false and slaveports["6380"] == true, "only 6380 should be found") r:slaveof("127.0.0.1", 6379) sentinel:close() } } --- request GET /t --- timeout: 10 --- no_error_log [error] === TEST 4: connector.connect_via_sentinel --- http_config eval: $::HttpConfig --- config location /t { content_by_lua_block { local rc = require("resty.redis.connector").new() local params = { sentinels = { { host = "127.0.0.1", port = 6381 }, { host = "127.0.0.1", port = 6382 }, { host = "127.0.0.1", port = 6383 }, }, master_name = "mymaster", role = "master", } local redis, err = rc:connect_via_sentinel(params) assert(redis and not err, "redis should connect without error") params.role = "slave" local redis, err = rc:connect_via_sentinel(params) assert(redis and not err, "redis should connect without error") } } --- request GET /t --- no_error_log [error] === TEST 5: regression for slave sorting (iss12) --- http_config eval: $::HttpConfig --- config location /t { lua_socket_log_errors Off; content_by_lua_block { local rc = require("resty.redis.connector").new() local params = { sentinels = { { host = "127.0.0.1", port = 6381 }, { host = "127.0.0.1", port = 6382 }, { host = "127.0.0.1", port = 6383 }, }, master_name = "mymaster", role = "slave", } -- hotwire get_slaves to expose sorting issue local sentinel = require("resty.redis.sentinel") sentinel.get_slaves = function() return { { host = "127.0.0.1", port = 6380 }, { host = "127.0.0.1", port = 6378 }, { host = "127.0.0.1", port = 6377 }, { host = "134.123.51.2", port = 6380 }, } end local redis, err = rc:connect_via_sentinel(params) assert(redis and not err, "redis should connect without error") } } --- request GET /t --- no_error_log [error] lua-resty-redis-connector-0.06/util/000077500000000000000000000000001314506577200174475ustar00rootroot00000000000000lua-resty-redis-connector-0.06/util/lua-releng000077500000000000000000000040611314506577200214310ustar00rootroot00000000000000#!/usr/bin/env perl use strict; use warnings; sub file_contains ($$); my $version; for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) { # Check the sanity of each .lua file open my $in, $file or die "ERROR: Can't open $file for reading: $!\n"; my $found_ver; while (<$in>) { my ($ver, $skipping); if (/(?x) (?:_VERSION) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { my $orig_ver = $ver = $1; $found_ver = 1; # $skipping = $2; $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; warn "$file: $orig_ver ($ver)\n"; } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { warn "$file: $1\n"; $found_ver = 1; last; } if ($ver and $version and !$skipping) { if ($version ne $ver) { # die "$file: $ver != $version\n"; } } elsif ($ver and !$version) { $version = $ver; } } if (!$found_ver) { warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; } close $in; #print "Checking use of Lua global variables in file $file ...\n"; system("luac5.1 -p -l $file | grep ETGLOBAL | grep -vE '(require|type|tostring|error|ngx|ndk|jit|setmetatable|getmetatable|string|table|io|os|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|select|rawset|rawget|debug)\$'"); #file_contains($file, "attempt to write to undeclared variable"); system("grep -H -n -E --color '.{120}' $file"); } sub file_contains ($$) { my ($file, $regex) = @_; open my $in, $file or die "Cannot open $file fo reading: $!\n"; my $content = do { local $/; <$in> }; close $in; #print "$content"; return scalar ($content =~ /$regex/); } if (-d 't') { for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); } }