mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
Squashed 'src/deps/src/lua-resty-redis-connector/' content from commit 02a29f93
git-subtree-dir: src/deps/src/lua-resty-redis-connector git-subtree-split: 02a29f93253d1f6ad392c5ac2b643c57e62b5979
This commit is contained in:
commit
8f051820b9
17 changed files with 2325 additions and 0 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.t linguist-language=lua
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
github: pintsized
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
t/servroot/
|
||||
t/error.log
|
||||
luacov.*
|
||||
*.src.rock
|
||||
lua-resty-redis-connector*.tar.gz
|
||||
2
.luacheckrc
Normal file
2
.luacheckrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
std = "ngx_lua"
|
||||
redefined = false
|
||||
4
.luacov
Normal file
4
.luacov
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
modules = {
|
||||
["resty.redis.connector"] = "lib/resty/redis/connector.lua",
|
||||
["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua",
|
||||
}
|
||||
84
.travis.yml
Normal file
84
.travis.yml
Normal 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
|
||||
189
Makefile
Normal file
189
Makefile
Normal 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)
|
||||
312
README.md
Normal file
312
README.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
# lua-resty-redis-connector
|
||||
|
||||
[](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.
|
||||
9
dist.ini
Normal file
9
dist.ini
Normal 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
|
||||
459
lib/resty/redis/connector.lua
Normal file
459
lib/resty/redis/connector.lua
Normal 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)
|
||||
63
lib/resty/redis/sentinel.lua
Normal file
63
lib/resty/redis/sentinel.lua
Normal 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
|
||||
27
lua-resty-redis-connector-0.11.0-0.rockspec
Normal file
27
lua-resty-redis-connector-0.11.0-0.rockspec
Normal 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"
|
||||
}
|
||||
}
|
||||
229
t/config.t
Normal file
229
t/config.t
Normal 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]
|
||||
442
t/connector.t
Normal file
442
t/connector.t
Normal 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]
|
||||
156
t/proxy.t
Normal file
156
t/proxy.t
Normal 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]
|
||||
279
t/sentinel.t
Normal file
279
t/sentinel.t
Normal 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]
|
||||
63
util/lua-releng
Executable file
63
util/lua-releng
Executable 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});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue