Merge commit '8f051820b9c885fd6bbe4c8fdbb0dc1f888aaae2' as 'src/deps/src/lua-resty-redis-connector'

This commit is contained in:
florian 2023-12-30 17:57:49 +01:00
commit 1b0c1cdb79
17 changed files with 2325 additions and 0 deletions

View file

@ -0,0 +1 @@
*.t linguist-language=lua

View file

@ -0,0 +1 @@
github: pintsized

View file

@ -0,0 +1,5 @@
t/servroot/
t/error.log
luacov.*
*.src.rock
lua-resty-redis-connector*.tar.gz

View file

@ -0,0 +1,2 @@
std = "ngx_lua"
redefined = false

View file

@ -0,0 +1,4 @@
modules = {
["resty.redis.connector"] = "lib/resty/redis/connector.lua",
["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua",
}

View file

@ -0,0 +1,84 @@
sudo: required
dist: focal
os: linux
language: c
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:redislabs/redis'
packages:
- luarocks
- lsof
cache:
directories:
- download-cache
env:
global:
- JOBS=3
- NGX_BUILD_JOBS=$JOBS
- LUAJIT_PREFIX=/opt/luajit21
- LUAJIT_LIB=$LUAJIT_PREFIX/lib
- LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1
- LUA_INCLUDE_DIR=$LUAJIT_INC
- OPENSSL_PREFIX=/opt/ssl
- OPENSSL_LIB=$OPENSSL_PREFIX/lib
- OPENSSL_INC=$OPENSSL_PREFIX/include
- OPENSSL_VER=1.1.1f
- LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH
- TEST_NGINX_SLEEP=0.006
- LUACHECK_VER=0.21.1
jobs:
- NGINX_VERSION=1.19.9
before_install:
# we can't update redis in addons.apt.packages as updated package automatically tries to start and immediately fails
- echo exit 101 | sudo tee /usr/sbin/policy-rc.d
- sudo chmod +x /usr/sbin/policy-rc.d
- sudo apt-get install -y redis-server
- sudo luarocks install luacov
- sudo luarocks install lua-resty-redis
- sudo luarocks install luacheck $LUACHECK_VER
- luacheck -q .
install:
- if [ ! -d download-cache ]; then mkdir download-cache; fi
- if [ ! -f download-cache/openssl-$OPENSSL_VER.tar.gz ]; then wget -O download-cache/openssl-$OPENSSL_VER.tar.gz https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz; fi
- sudo apt-get install -qq -y cpanminus axel
- sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1)
- git clone https://github.com/openresty/openresty.git ../openresty
- git clone https://github.com/openresty/nginx-devel-utils.git
- git clone https://github.com/openresty/lua-cjson.git
- git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module
- git clone https://github.com/openresty/stream-lua-nginx-module.git ../stream-lua-nginx-module
- git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core
- git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache
- git clone https://github.com/openresty/echo-nginx-module.git ../echo-nginx-module
- git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx
- git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git
script:
- sudo iptables -A OUTPUT -p tcp --dst 127.0.0.2 --dport 12345 -j DROP
- cd luajit2/
- make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' > build.log 2>&1 || (cat build.log && exit 1)
- sudo make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1)
- cd ../lua-cjson && make && sudo PATH=$PATH make install && cd ..
- tar zxf download-cache/openssl-$OPENSSL_VER.tar.gz
- cd openssl-$OPENSSL_VER/
- ./config shared --prefix=$OPENSSL_PREFIX -DPURIFY > build.log 2>&1 || (cat build.log && exit 1)
- make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1)
- sudo make PATH=$PATH install_sw > build.log 2>&1 || (cat build.log && exit 1)
- cd ..
- export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH
- export NGX_BUILD_CC=$CC
- ngx-build $NGINX_VERSION --with-ipv6 --with-http_realip_module --with-http_ssl_module --with-cc-opt="-I$OPENSSL_INC" --with-ld-opt="-L$OPENSSL_LIB -Wl,-rpath,$OPENSSL_LIB" --add-module=../echo-nginx-module --add-module=../lua-nginx-module --add-module=../stream-lua-nginx-module --with-stream --with-stream_ssl_module --with-debug > build.log 2>&1 || (cat build.log && exit 1)
- nginx -V
- ldd `which nginx`|grep -E 'luajit|ssl|pcre'
- mkdir -p tmp
- TMP_DIR=$PWD/tmp make test_all

View file

@ -0,0 +1,189 @@
SHELL := /bin/bash # Cheat by using bash :)
OPENRESTY_PREFIX = /usr/local/openresty
TEST_FILE ?= t
TMP_DIR ?= /tmp
REDIS_CMD = redis-server
SENTINEL_CMD = $(REDIS_CMD) --sentinel
REDIS_SOCK = /redis.sock
REDIS_PID = /redis.pid
REDIS_LOG = /redis.log
REDIS_PREFIX = $(TMP_DIR)/redis-
# Overrideable redis test variables
TEST_REDIS_PORT ?= 6380
TEST_REDIS_PORT_SL1 ?= 6381
TEST_REDIS_PORT_SL2 ?= 6382
TEST_REDIS_PORT_AUTH ?= 6383
TEST_REDIS_PORTS ?= $(TEST_REDIS_PORT) $(TEST_REDIS_PORT_SL1) $(TEST_REDIS_PORT_SL2)
TEST_REDIS_PORTS_ALL ?= $(TEST_REDIS_PORTS) $(TEST_REDIS_PORT_AUTH)
TEST_REDIS_DATABASE ?= 1
TEST_REDIS_SOCKET ?= $(REDIS_PREFIX)$(TEST_REDIS_PORT)$(REDIS_SOCK)
REDIS_SLAVE_ARG := --slaveof 127.0.0.1 $(TEST_REDIS_PORT)
REDIS_CLI := redis-cli -p $(TEST_REDIS_PORT) -n $(TEST_REDIS_DATABASE)
# Overrideable redis + sentinel test variables
TEST_SENTINEL_PORT1 ?= 6390
TEST_SENTINEL_PORT2 ?= 6391
TEST_SENTINEL_PORT3 ?= 6392
TEST_SENTINEL_PORT_AUTH ?= 6393
TEST_SENTINEL_PORTS ?= $(TEST_SENTINEL_PORT1) $(TEST_SENTINEL_PORT2) $(TEST_SENTINEL_PORT3)
TEST_SENTINEL_PORTS_ALL ?= $(TEST_SENTINEL_PORTS) $(TEST_SENTINEL_PORT_AUTH)
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_NGINX_REDIS_PORT=$(TEST_REDIS_PORT) \
TEST_NGINX_REDIS_PORT_SL1=$(TEST_REDIS_PORT_SL1) \
TEST_NGINX_REDIS_PORT_SL2=$(TEST_REDIS_PORT_SL2) \
TEST_NGINX_REDIS_PORT_AUTH=$(TEST_REDIS_PORT_AUTH) \
TEST_NGINX_REDIS_SOCKET=unix:$(TEST_REDIS_SOCKET) \
TEST_NGINX_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_NGINX_REDIS_PORT=$(TEST_NGINX_REDIS_PORT) \
TEST_NGINX_REDIS_PORT_SL1=$(TEST_NGINX_REDIS_PORT_SL1) \
TEST_NGINX_REDIS_PORT_SL2=$(TEST_NGINX_REDIS_PORT_SL2) \
TEST_NGINX_SENTINEL_PORT1=$(TEST_NGINX_SENTINEL_PORT1) \
TEST_NGINX_SENTINEL_PORT2=$(TEST_NGINX_SENTINEL_PORT2) \
TEST_NGINX_SENTINEL_PORT3=$(TEST_NGINX_SENTINEL_PORT3) \
TEST_NGINX_SENTINEL_PORT_AUTH=$(TEST_NGINX_SENTINEL_AUTH) \
TEST_NGINX_SENTINEL_MASTER_NAME=$(TEST_NGINX_SENTINEL_MASTER_NAME) \
TEST_NGINX_REDIS_DATABASE=$(TEST_NGINX_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 $(TEST_REDIS_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
define TEST_SENTINEL_AUTH_CONFIG
sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(TEST_REDIS_PORT_AUTH) 1
endef
export TEST_SENTINEL_CONFIG TEST_SENTINEL_AUTH_CONFIG
SENTINEL_CONFIG_FILE = /tmp/sentinel-test-config
SENTINEL_AUTH_CONFIG_FILE = /tmp/sentinel-auth-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
check:
luacheck lib
sleep:
sleep 3
start_redis_instances: check_ports create_sentinel_config
$(REDIS_CMD) --version
@$(foreach port,$(TEST_REDIS_PORTS), \
[[ "$(port)" != "$(TEST_REDIS_PORT)" ]] && \
SLAVE="$(REDIS_SLAVE_ARG)" || \
SLAVE="" && \
$(MAKE) start_redis_instance args="$$SLAVE" port=$(port) \
prefix=$(REDIS_PREFIX)$(port) && \
) true
$(MAKE) start_redis_instance \
args="--user redisuser on '>redisuserpass' '~*' '&*' '+@all'" \
port=$(TEST_REDIS_PORT_AUTH) \
prefix=$(REDIS_PREFIX)$(TEST_REDIS_PORT_AUTH)
@$(foreach port,$(TEST_SENTINEL_PORTS), \
$(MAKE) start_redis_instance \
port=$(port) args="$(SENTINEL_CONFIG_FILE) --sentinel" \
prefix=$(REDIS_PREFIX)$(port) && \
) true
$(MAKE) start_redis_instance \
args="$(SENTINEL_AUTH_CONFIG_FILE) --sentinel --user sentineluser on '>sentineluserpass' '~*' '&*' '+@all'" \
port=$(TEST_SENTINEL_PORT_AUTH) \
prefix=$(REDIS_PREFIX)$(TEST_SENTINEL_PORT_AUTH)
stop_redis_instances: delete_sentinel_config
-@$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL), \
$(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)
-@echo "Creating $(SENTINEL_AUTH_CONFIG_FILE)"
@echo "$$TEST_SENTINEL_AUTH_CONFIG" > $(SENTINEL_AUTH_CONFIG_FILE)
delete_sentinel_config:
-@echo "Removing $(SENTINEL_CONFIG_FILE)"
@rm -f $(SENTINEL_CONFIG_FILE)
-@echo "Removing $(SENTINEL_AUTH_CONFIG_FILE)"
@rm -f $(SENTINEL_AUTH_CONFIG_FILE)
check_ports:
-@echo "Checking ports $(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL)"
@$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL),! 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)

View file

@ -0,0 +1,312 @@
# lua-resty-redis-connector
[![Build
Status](https://travis-ci.org/ledgetech/lua-resty-redis-connector.svg?branch=master)](https://travis-ci.org/ledgetech/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,
send_timeout = 5000,
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,
send_timeout = 5000,
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://USERNAME:PASSWORD@HOST:PORT/DB`
The `USERNAME`, `PASSWORD` and `DB` fields are optional, all other components
are required.
Use of username requires Redis 6.0.0 or newer.
### Connections via Redis Sentinel
When connecting via Redis Sentinel, the format is as follows:
`sentinel://USERNAME:PASSWORD@MASTER_NAME:ROLE/DB`
Again, `USERNAME`, `PASSWORD` and `DB` are optional. `ROLE` must be either `m`
or `s` for master / slave respectively.
On versions of Redis newer than 5.0.1, Sentinels can optionally require their
own password. If enabled, provide this password in the `sentinel_password`
parameter. On Redis 6.2.0 and newer you can pass username using
`sentinel_username` parameter.
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 },
},
sentinel_username = "default",
sentinel_password = "password"
}
```
## 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,
send_timeout = 1000,
read_timeout = 1000,
keepalive_timeout = 60000,
keepalive_poolsize = 30,
-- ssl, ssl_verify, server_name, pool, pool_size, backlog
-- see: https://github.com/openresty/lua-resty-redis#connect
connection_options = {},
host = "127.0.0.1",
port = "6379",
path = "", -- unix socket path, e.g. /tmp/redis.sock
username = "",
password = "",
sentinel_username = "",
sentinel_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 <james@pintsized.co.uk>
# Licence
This module is licensed under the 2-clause BSD license.
Copyright (c) James Hurst <james@pintsized.co.uk>
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.

View file

@ -0,0 +1,9 @@
name=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/ledgetech/lua-resty-redis-connector
main_module=lib/resty/redis/connector.lua

View file

@ -0,0 +1,459 @@
local 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 str_find = string.find
local str_sub = string.sub
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 -- luacheck: ignore 212
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) -- luacheck: ignore 212
error("field " .. tostring(k) .. " does not exist", 3)
end,
__newindex =
function(t, k, v) -- luacheck: ignore 212
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 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,
send_timeout = 1000,
connection_options = {}, -- pool, etc
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = 6379,
path = "", -- /tmp/redis.sock
username = "",
password = "",
sentinel_username = "",
sentinel_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.11.0',
}
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
-- username/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
if v == "db" or v == "port" then
parsed_params[v] = tonumber(m[i + 1])
else
parsed_params[v] = m[i + 1]
end
if v == "role" then
parsed_params[v] = roles[parsed_params[v]]
end
end
local colon_pos = str_find(parsed_params.password or "", ":", 1, true)
if colon_pos then
parsed_params.username = str_sub(parsed_params.password, 1, colon_pos - 1)
parsed_params.password = str_sub(parsed_params.password, colon_pos + 1)
end
return tbl_copy_merge_defaults(params, parsed_params)
end
return params
end
_M.parse_dsn = parse_dsn
-- Fill out gaps in config with any dsn params
local function apply_dsn(config)
if config and config.url then
local err
config, err = parse_dsn(config)
if err then ngx_log(ngx_ERR, err) end
end
return config
end
-- For backwards compatability; previously send_timeout was implicitly the
-- same as read_timeout. So if only the latter is given, ensure the former
-- matches.
local function apply_fallback_send_timeout(config)
if config and not config.send_timeout and config.read_timeout then
config.send_timeout = config.read_timeout
end
end
function _M.new(config)
config = apply_dsn(config)
apply_fallback_send_timeout(config)
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)
params = apply_dsn(params)
apply_fallback_send_timeout(params)
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 username = params.username
local password = params.password
local sentinel_username = params.sentinel_username
local sentinel_password = params.sentinel_password
if sentinel_password then
for _, host in ipairs(sentinels) do
host.username = sentinel_username
host.password = sentinel_password
end
end
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 not master then
return nil, err
end
sentnl:set_keepalive()
master.db = db
master.username = username
master.password = password
local redis, err = self:connect_to_host(master)
if not redis then
return nil, err
end
return redis
else
-- We want a slave
local slaves, err = get_slaves(sentnl, master_name)
if not slaves then
return nil, err
end
sentnl:set_keepalive()
-- Put any slaves on 127.0.0.1 at the front
tbl_sort(slaves, sort_by_localhost)
if db or password then
for _, slave in ipairs(slaves) do
slave.db = db
slave.username = username
slave.password = password
end
end
local slave, err, previous_errors = self:try_hosts(slaves)
if not slave then
return nil, err, previous_errors
end
return slave
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()
-- config options in 'host' should override the global defaults
-- host contains keys that aren't in config
-- this can break tbl_copy_merge_defaults, hence the mannual loop here
local config = tbl_copy(self.config)
for k, _ in pairs(config) do
if host[k] then
config[k] = host[k]
end
end
r:set_timeouts(
config.connect_timeout,
config.send_timeout,
config.read_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
local username = host.username
local password = host.password
if password and password ~= "" then
local res
-- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary
if username and username ~= "" and username ~= "default" then
res, err = r:auth(username, password)
else
res, err = r:auth(password)
end
if err then
return res, err
end
end
-- No support for DBs in proxied Redis.
if config.connection_is_proxied ~= true and host.db ~= nil then
local res, err = r:select(host.db)
-- SELECT will fail if we are connected to sentinel:
-- detect it and ignore error message it that's the case
if err and str_find(err, "ERR unknown command") then
local role = r:role()
if role and role[1] == "sentinel" then
err = nil
end
end
if err then
return res, err
end
end
return r, nil
end
end
function _M.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
-- 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)

View file

@ -0,0 +1,63 @@
local 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 -- luacheck: ignore 212
end
local _M = {
_VERSION = '0.11.0'
}
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] }
elseif res == ngx_null then
return nil, "invalid master name"
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
local master_link_status_ok = host["master-link-status"] == "ok"
local is_down = host["flags"] and (string.find(host["flags"],"s_down")
or string.find(host["flags"],"disconnected"))
if master_link_status_ok and not is_down 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

View file

@ -0,0 +1,27 @@
package = "lua-resty-redis-connector"
version = "0.11.0-0"
source = {
url = "git://github.com/ledgetech/lua-resty-redis-connector",
tag = "v0.11.0"
}
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/ledgetech/lua-resty-redis-connector",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
}
dependencies = {
"lua >= 5.1",
}
build = {
type = "builtin",
modules = {
["resty.redis.connector"] = "lib/resty/redis/connector.lua",
["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua"
}
}

View file

@ -0,0 +1,229 @@
use 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;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
no_long_string();
run_tests();
__DATA__
=== TEST 1: Default config
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = assert(require("resty.redis.connector").new())
}
}
--- 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 = $TEST_NGINX_REDIS_PORT,
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 = $TEST_NGINX_REDIS_PORT,
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, err = rc:connect({
port = $TEST_NGINX_REDIS_PORT,
disabled_commands = { "set" }
})
if not redis or err then
ngx.log(ngx.ERR, "connect failed: ", err)
return
end
local ok, err = redis:set("foo", "bar")
assert( ok == nil and (string.find(err, "disabled") ~= nil) , "Disabled commands not passed through" )
}
}
--- 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 = $TEST_NGINX_REDIS_PORT,
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,
send_timeout = 500,
read_timeout = 1000,
connection_options = { pool = "<host>::<port>" },
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = $TEST_NGINX_REDIS_PORT,
path = "",
username = "",
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,
send_timeout = 500,
read_timeout = 1000,
connection_options = { pool = "<host>::<port>" },
keepalive_timeout = 60000,
keepalive_poolsize = 30,
host = "127.0.0.1",
port = $TEST_NGINX_REDIS_PORT,
path = "",
username = "",
password = "",
db = 0,
url = "",
master_name = "mymaster",
role = "master",
sentinels = {},
}), "rc:connect should return positively")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 5: timeout defaults
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
-- global defaults
local rc = require("resty.redis.connector").new({
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 1000, "send_timeout should be 1000")
assert(rc.config.read_timeout == 1000, "read_timeout should be 1000")
local redis = assert(rc:connect(), "rc:connect should return positively")
assert(redis:set("foo", "bar"))
rc:set_keepalive(redis)
-- send_timeout defaults to read_timeout
rc = require("resty.redis.connector").new({
read_timeout = 500,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 500, "send_timeout should be 500")
assert(rc.config.read_timeout == 500, "read_timeout should be 500")
local redis = assert(rc:connect(), "rc:connect should return positively")
assert(redis:set("foo", "bar"))
rc:set_keepalive(redis)
-- send_timeout can be set separately from read_timeout
rc = require("resty.redis.connector").new({
send_timeout = 500,
read_timeout = 200,
port = $TEST_NGINX_REDIS_PORT,
db = 6,
keepalive_poolsize = 10,
})
assert(rc.config.connect_timeout == 100, "connect_timeout should be 100")
assert(rc.config.send_timeout == 500, "send_timeout should be 500")
assert(rc.config.read_timeout == 200, "read_timeout should be 200")
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,442 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
$ENV{TEST_NGINX_REDIS_PORT_AUTH} ||= 6393;
$ENV{TEST_NGINX_REDIS_SOCKET} ||= 'unix://tmp/redis/redis.sock';
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(string.len(previous_errors[1]) > 0,
"previous_errors[1] should contain an error")
assert(string.len(previous_errors[2]) > 0,
"previous_errors[2] should contain an error")
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(string.len(previous_errors[1]) > 0,
"previous_errors[1] should contain an error")
assert(string.len(previous_errors[2]) > 0,
"previous_errors[2] should contain an error")
}
}
--- 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") and string.find(err, "AUTH"),
"connect should fail with password error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 7: username and 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,
username = "x",
password = "foo",
})
local redis, err = rc:connect()
assert(not redis and string.find(err, "WRONGPASS"),
"connect should fail with invalid username-password error")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 8: Bad unix domain socket path should fail
--- 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://GARBAGE_PATH_AKFDKAJSFKJSAFLKJSADFLKJSANCKAJSNCKJSANCLKAJS",
}):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.1: Good unix domain socket path should succeed
--- 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 = "$TEST_NGINX_REDIS_SOCKET",
}):connect()
assert (redis and not err,
"connection should be valid")
redis:close()
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 9: 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:bar@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")
assert(params.username == "foo", "username should be foo")
assert(params.password == "bar", "password should be bar")
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 10: 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:$TEST_NGINX_REDIS_PORT/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) == $TEST_NGINX_REDIS_PORT, "port should still be $TEST_NGINX_REDIS_PORT")
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 11: 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")
local redis, err = rc2:connect({
url = "redis://redisuser:redisuserpass@127.0.0.1:$TEST_NGINX_REDIS_PORT_AUTH/"
})
assert(redis and not err, "connect should return positively")
local username = assert(redis:acl("whoami"))
assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username))
}
}
--- request
GET /t
--- no_error_log
[error]
=== TEST 12: DSN without DB
--- 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",
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")
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,156 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
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]

View file

@ -0,0 +1,279 @@
use Test::Nginx::Socket 'no_plan';
use Cwd qw(cwd);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
lua_socket_log_errors Off;
init_by_lua_block {
require("luacov.runner").init()
}
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
$ENV{TEST_NGINX_REDIS_PORT} ||= 6380;
$ENV{TEST_NGINX_REDIS_PORT_SL1} ||= 6381;
$ENV{TEST_NGINX_REDIS_PORT_SL2} ||= 6382;
$ENV{TEST_NGINX_SENTINEL_PORT1} ||= 6390;
$ENV{TEST_NGINX_SENTINEL_PORT2} ||= 6391;
$ENV{TEST_NGINX_SENTINEL_PORT3} ||= 6392;
$ENV{TEST_NGINX_SENTINEL_PORT_AUTH} ||= 6393;
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 rs = require("resty.redis.sentinel")
local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1" }
assert(sentinel and not err, "sentinel should connect without errors but got " .. tostring(err))
local master, err = rs.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) == $TEST_NGINX_REDIS_PORT,
"host should be 127.0.0.1 and port should be $TEST_NGINX_REDIS_PORT")
master, err = rs.get_master(sentinel, "invalid-mymaster")
assert(not master and err, "invalid master name should result in error")
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 = $TEST_NGINX_SENTINEL_PORT1 }
}
})
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 rs = require("resty.redis.sentinel")
local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1" }
assert(sentinel and not err, "sentinel should connect without error")
local slaves, err = rs.get_slaves(sentinel, "mymaster")
assert(slaves and not err, "slaves should be returned without error")
local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"slaves should both be found")
slaves, err = rs.get_slaves(sentinel, "invalid-mymaster")
assert(not slaves and err, "invalid master name should result in error")
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:$TEST_NGINX_SENTINEL_PORT1" })
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 = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"slaves should both be found")
-- connect to one and remove it
local r = require("resty.redis.connector").new():connect({
port = $TEST_NGINX_REDIS_PORT_SL1,
})
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 = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false }
for _,slave in ipairs(slaves) do
slaveports[tostring(slave.port)] = true
end
assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == false and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true,
"only $TEST_NGINX_REDIS_PORT_SL2 should be found")
r:slaveof("127.0.0.1", $TEST_NGINX_REDIS_PORT)
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 = $TEST_NGINX_SENTINEL_PORT1 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 },
},
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 = $TEST_NGINX_SENTINEL_PORT1 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 },
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 },
},
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 = $TEST_NGINX_REDIS_PORT_SL1 },
{ host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT_SL2 },
{ host = "134.123.51.2", port = $TEST_NGINX_REDIS_PORT_SL1 },
}
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]
=== TEST 6: connect with acl
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua_block {
local rc = require("resty.redis.connector").new()
local redis, err = rc:connect({
username = "redisuser",
password = "redisuserpass",
sentinels = {
{ host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT_AUTH }
},
master_name = "mymaster",
sentinel_username = "sentineluser",
sentinel_username = "sentineluserpass",
})
assert(redis and not err, "redis should connect without error")
local username = assert(redis:acl("whoami"))
assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username))
}
}
--- request
GET /t
--- no_error_log
[error]

View file

@ -0,0 +1,63 @@
#!/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("luac -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});
}
}