├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .luacheckrc ├── .luacov ├── .travis.yml ├── Makefile ├── README.md ├── dist.ini ├── lib └── resty │ └── redis │ ├── connector.lua │ └── sentinel.lua ├── lua-resty-redis-connector-0.11.0-0.rockspec ├── t ├── config.t ├── connector.t ├── proxy.t └── sentinel.t └── util └── lua-releng /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=lua 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: pintsized 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/ 2 | t/error.log 3 | luacov.* 4 | *.src.rock 5 | lua-resty-redis-connector*.tar.gz 6 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = "ngx_lua" 2 | redefined = false 3 | -------------------------------------------------------------------------------- /.luacov: -------------------------------------------------------------------------------- 1 | modules = { 2 | ["resty.redis.connector"] = "lib/resty/redis/connector.lua", 3 | ["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua", 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: focal 3 | 4 | os: linux 5 | 6 | language: c 7 | 8 | compiler: gcc 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - sourceline: 'ppa:redislabs/redis' 14 | packages: 15 | - luarocks 16 | - lsof 17 | 18 | cache: 19 | directories: 20 | - download-cache 21 | 22 | env: 23 | global: 24 | - JOBS=3 25 | - NGX_BUILD_JOBS=$JOBS 26 | - LUAJIT_PREFIX=/opt/luajit21 27 | - LUAJIT_LIB=$LUAJIT_PREFIX/lib 28 | - LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1 29 | - LUA_INCLUDE_DIR=$LUAJIT_INC 30 | - OPENSSL_PREFIX=/opt/ssl 31 | - OPENSSL_LIB=$OPENSSL_PREFIX/lib 32 | - OPENSSL_INC=$OPENSSL_PREFIX/include 33 | - OPENSSL_VER=1.1.1f 34 | - LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH 35 | - TEST_NGINX_SLEEP=0.006 36 | - LUACHECK_VER=0.21.1 37 | jobs: 38 | - NGINX_VERSION=1.19.9 39 | 40 | before_install: 41 | # we can't update redis in addons.apt.packages as updated package automatically tries to start and immediately fails 42 | - echo exit 101 | sudo tee /usr/sbin/policy-rc.d 43 | - sudo chmod +x /usr/sbin/policy-rc.d 44 | - sudo apt-get install -y redis-server 45 | - sudo luarocks install luacov 46 | - sudo luarocks install lua-resty-redis 47 | - sudo luarocks install luacheck $LUACHECK_VER 48 | - luacheck -q . 49 | 50 | install: 51 | - if [ ! -d download-cache ]; then mkdir download-cache; fi 52 | - 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 53 | - sudo apt-get install -qq -y cpanminus axel 54 | - sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) 55 | - git clone https://github.com/openresty/openresty.git ../openresty 56 | - git clone https://github.com/openresty/nginx-devel-utils.git 57 | - git clone https://github.com/openresty/lua-cjson.git 58 | - git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module 59 | - git clone https://github.com/openresty/stream-lua-nginx-module.git ../stream-lua-nginx-module 60 | - git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core 61 | - git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache 62 | - git clone https://github.com/openresty/echo-nginx-module.git ../echo-nginx-module 63 | - git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx 64 | - git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git 65 | 66 | script: 67 | - sudo iptables -A OUTPUT -p tcp --dst 127.0.0.2 --dport 12345 -j DROP 68 | - cd luajit2/ 69 | - 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) 70 | - sudo make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1) 71 | - cd ../lua-cjson && make && sudo PATH=$PATH make install && cd .. 72 | - tar zxf download-cache/openssl-$OPENSSL_VER.tar.gz 73 | - cd openssl-$OPENSSL_VER/ 74 | - ./config shared --prefix=$OPENSSL_PREFIX -DPURIFY > build.log 2>&1 || (cat build.log && exit 1) 75 | - make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 76 | - sudo make PATH=$PATH install_sw > build.log 2>&1 || (cat build.log && exit 1) 77 | - cd .. 78 | - export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH 79 | - export NGX_BUILD_CC=$CC 80 | - 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) 81 | - nginx -V 82 | - ldd `which nginx`|grep -E 'luajit|ssl|pcre' 83 | - mkdir -p tmp 84 | - TMP_DIR=$PWD/tmp make test_all 85 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash # Cheat by using bash :) 2 | 3 | OPENRESTY_PREFIX = /usr/local/openresty 4 | 5 | TEST_FILE ?= t 6 | TMP_DIR ?= /tmp 7 | 8 | REDIS_CMD = redis-server 9 | SENTINEL_CMD = $(REDIS_CMD) --sentinel 10 | 11 | REDIS_SOCK = /redis.sock 12 | REDIS_PID = /redis.pid 13 | REDIS_LOG = /redis.log 14 | REDIS_PREFIX = $(TMP_DIR)/redis- 15 | 16 | # Overrideable redis test variables 17 | TEST_REDIS_PORT ?= 6380 18 | TEST_REDIS_PORT_SL1 ?= 6381 19 | TEST_REDIS_PORT_SL2 ?= 6382 20 | TEST_REDIS_PORT_AUTH ?= 6383 21 | TEST_REDIS_PORTS ?= $(TEST_REDIS_PORT) $(TEST_REDIS_PORT_SL1) $(TEST_REDIS_PORT_SL2) 22 | TEST_REDIS_PORTS_ALL ?= $(TEST_REDIS_PORTS) $(TEST_REDIS_PORT_AUTH) 23 | TEST_REDIS_DATABASE ?= 1 24 | TEST_REDIS_SOCKET ?= $(REDIS_PREFIX)$(TEST_REDIS_PORT)$(REDIS_SOCK) 25 | 26 | REDIS_SLAVE_ARG := --slaveof 127.0.0.1 $(TEST_REDIS_PORT) 27 | REDIS_CLI := redis-cli -p $(TEST_REDIS_PORT) -n $(TEST_REDIS_DATABASE) 28 | 29 | # Overrideable redis + sentinel test variables 30 | TEST_SENTINEL_PORT1 ?= 6390 31 | TEST_SENTINEL_PORT2 ?= 6391 32 | TEST_SENTINEL_PORT3 ?= 6392 33 | TEST_SENTINEL_PORT_AUTH ?= 6393 34 | TEST_SENTINEL_PORTS ?= $(TEST_SENTINEL_PORT1) $(TEST_SENTINEL_PORT2) $(TEST_SENTINEL_PORT3) 35 | TEST_SENTINEL_PORTS_ALL ?= $(TEST_SENTINEL_PORTS) $(TEST_SENTINEL_PORT_AUTH) 36 | TEST_SENTINEL_MASTER_NAME ?= mymaster 37 | TEST_SENTINEL_PROMOTION_TIME ?= 20 38 | 39 | # Command line arguments for redis tests 40 | TEST_REDIS_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ 41 | TEST_NGINX_REDIS_PORT=$(TEST_REDIS_PORT) \ 42 | TEST_NGINX_REDIS_PORT_SL1=$(TEST_REDIS_PORT_SL1) \ 43 | TEST_NGINX_REDIS_PORT_SL2=$(TEST_REDIS_PORT_SL2) \ 44 | TEST_NGINX_REDIS_PORT_AUTH=$(TEST_REDIS_PORT_AUTH) \ 45 | TEST_NGINX_REDIS_SOCKET=unix:$(TEST_REDIS_SOCKET) \ 46 | TEST_NGINX_REDIS_DATABASE=$(TEST_REDIS_DATABASE) \ 47 | TEST_NGINX_NO_SHUFFLE=1 48 | 49 | # Command line arguments for sentinel tests 50 | TEST_SENTINEL_VARS = PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$(PATH) \ 51 | TEST_NGINX_REDIS_PORT=$(TEST_NGINX_REDIS_PORT) \ 52 | TEST_NGINX_REDIS_PORT_SL1=$(TEST_NGINX_REDIS_PORT_SL1) \ 53 | TEST_NGINX_REDIS_PORT_SL2=$(TEST_NGINX_REDIS_PORT_SL2) \ 54 | TEST_NGINX_SENTINEL_PORT1=$(TEST_NGINX_SENTINEL_PORT1) \ 55 | TEST_NGINX_SENTINEL_PORT2=$(TEST_NGINX_SENTINEL_PORT2) \ 56 | TEST_NGINX_SENTINEL_PORT3=$(TEST_NGINX_SENTINEL_PORT3) \ 57 | TEST_NGINX_SENTINEL_PORT_AUTH=$(TEST_NGINX_SENTINEL_AUTH) \ 58 | TEST_NGINX_SENTINEL_MASTER_NAME=$(TEST_NGINX_SENTINEL_MASTER_NAME) \ 59 | TEST_NGINX_REDIS_DATABASE=$(TEST_NGINX_REDIS_DATABASE) \ 60 | TEST_NGINX_NO_SHUFFLE=1 61 | 62 | # Sentinel configuration can only be set by a config file 63 | define TEST_SENTINEL_CONFIG 64 | sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(TEST_REDIS_PORT) 2 65 | sentinel down-after-milliseconds $(TEST_SENTINEL_MASTER_NAME) 2000 66 | sentinel failover-timeout $(TEST_SENTINEL_MASTER_NAME) 10000 67 | sentinel parallel-syncs $(TEST_SENTINEL_MASTER_NAME) 5 68 | endef 69 | define TEST_SENTINEL_AUTH_CONFIG 70 | sentinel monitor $(TEST_SENTINEL_MASTER_NAME) 127.0.0.1 $(TEST_REDIS_PORT_AUTH) 1 71 | endef 72 | 73 | export TEST_SENTINEL_CONFIG TEST_SENTINEL_AUTH_CONFIG 74 | 75 | SENTINEL_CONFIG_FILE = /tmp/sentinel-test-config 76 | SENTINEL_AUTH_CONFIG_FILE = /tmp/sentinel-auth-test-config 77 | 78 | 79 | PREFIX ?= /usr/local 80 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 81 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 82 | PROVE ?= prove -I ../test-nginx/lib 83 | INSTALL ?= install 84 | 85 | .PHONY: all install test test_all start_redis_instances stop_redis_instances \ 86 | start_redis_instance stop_redis_instance cleanup_redis_instance flush_db \ 87 | create_sentinel_config delete_sentinel_config check_ports test_redis \ 88 | test_sentinel sleep 89 | 90 | all: ; 91 | 92 | install: all 93 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis 94 | $(INSTALL) lib/resty/redis/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/redis 95 | 96 | test: test_redis 97 | test_all: start_redis_instances sleep test_redis stop_redis_instances 98 | 99 | check: 100 | luacheck lib 101 | 102 | sleep: 103 | sleep 3 104 | 105 | start_redis_instances: check_ports create_sentinel_config 106 | $(REDIS_CMD) --version 107 | 108 | @$(foreach port,$(TEST_REDIS_PORTS), \ 109 | [[ "$(port)" != "$(TEST_REDIS_PORT)" ]] && \ 110 | SLAVE="$(REDIS_SLAVE_ARG)" || \ 111 | SLAVE="" && \ 112 | $(MAKE) start_redis_instance args="$$SLAVE" port=$(port) \ 113 | prefix=$(REDIS_PREFIX)$(port) && \ 114 | ) true 115 | 116 | $(MAKE) start_redis_instance \ 117 | args="--user redisuser on '>redisuserpass' '~*' '&*' '+@all'" \ 118 | port=$(TEST_REDIS_PORT_AUTH) \ 119 | prefix=$(REDIS_PREFIX)$(TEST_REDIS_PORT_AUTH) 120 | 121 | @$(foreach port,$(TEST_SENTINEL_PORTS), \ 122 | $(MAKE) start_redis_instance \ 123 | port=$(port) args="$(SENTINEL_CONFIG_FILE) --sentinel" \ 124 | prefix=$(REDIS_PREFIX)$(port) && \ 125 | ) true 126 | 127 | $(MAKE) start_redis_instance \ 128 | args="$(SENTINEL_AUTH_CONFIG_FILE) --sentinel --user sentineluser on '>sentineluserpass' '~*' '&*' '+@all'" \ 129 | port=$(TEST_SENTINEL_PORT_AUTH) \ 130 | prefix=$(REDIS_PREFIX)$(TEST_SENTINEL_PORT_AUTH) 131 | 132 | stop_redis_instances: delete_sentinel_config 133 | -@$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL), \ 134 | $(MAKE) stop_redis_instance cleanup_redis_instance port=$(port) \ 135 | prefix=$(REDIS_PREFIX)$(port) && \ 136 | ) true 2>&1 > /dev/null 137 | 138 | 139 | start_redis_instance: 140 | -@echo "Starting redis on port $(port) with args: \"$(args)\"" 141 | -@mkdir -p $(prefix) 142 | $(REDIS_CMD) $(args) \ 143 | --pidfile $(prefix)$(REDIS_PID) \ 144 | --bind 127.0.0.1 --port $(port) \ 145 | --unixsocket $(prefix)$(REDIS_SOCK) \ 146 | --unixsocketperm 777 \ 147 | --dir $(prefix) \ 148 | --logfile $(prefix)$(REDIS_LOG) \ 149 | --loglevel debug \ 150 | --daemonize yes 151 | 152 | stop_redis_instance: 153 | -@echo "Stopping redis on port $(port)" 154 | -@[[ -f "$(prefix)$(REDIS_PID)" ]] && kill -QUIT \ 155 | `cat $(prefix)$(REDIS_PID)` 2>&1 > /dev/null || true 156 | 157 | cleanup_redis_instance: stop_redis_instance 158 | -@echo "Cleaning up redis files in $(prefix)" 159 | -@rm -rf $(prefix) 160 | 161 | flush_db: 162 | -@echo "Flushing Redis DB" 163 | @$(REDIS_CLI) flushdb 164 | 165 | create_sentinel_config: 166 | -@echo "Creating $(SENTINEL_CONFIG_FILE)" 167 | @echo "$$TEST_SENTINEL_CONFIG" > $(SENTINEL_CONFIG_FILE) 168 | -@echo "Creating $(SENTINEL_AUTH_CONFIG_FILE)" 169 | @echo "$$TEST_SENTINEL_AUTH_CONFIG" > $(SENTINEL_AUTH_CONFIG_FILE) 170 | 171 | delete_sentinel_config: 172 | -@echo "Removing $(SENTINEL_CONFIG_FILE)" 173 | @rm -f $(SENTINEL_CONFIG_FILE) 174 | -@echo "Removing $(SENTINEL_AUTH_CONFIG_FILE)" 175 | @rm -f $(SENTINEL_AUTH_CONFIG_FILE) 176 | 177 | check_ports: 178 | -@echo "Checking ports $(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL)" 179 | @$(foreach port,$(TEST_REDIS_PORTS_ALL) $(TEST_SENTINEL_PORTS_ALL),! lsof -i :$(port) &&) true 2>&1 > /dev/null 180 | 181 | test_redis: flush_db 182 | util/lua-releng 183 | @rm -f luacov.stats.out 184 | $(TEST_REDIS_VARS) $(PROVE) $(TEST_FILE) 185 | @luacov 186 | @tail -7 luacov.report.out 187 | 188 | test_leak: flush_db 189 | $(TEST_REDIS_VARS) TEST_NGINX_CHECK_LEAK=1 $(PROVE) $(TEST_FILE) 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-redis-connector 2 | 3 | [![Build 4 | Status](https://travis-ci.org/ledgetech/lua-resty-redis-connector.svg?branch=master)](https://travis-ci.org/ledgetech/lua-resty-redis-connector) 5 | 6 | Connection utilities for 7 | [lua-resty-redis](https://github.com/openresty/lua-resty-redis), making it easy 8 | and reliable to connect to Redis hosts, either directly or via [Redis 9 | Sentinel](http://redis.io/topics/sentinel). 10 | 11 | 12 | ## Synopsis 13 | 14 | Quick and simple authenticated connection on localhost to DB 2: 15 | 16 | ```lua 17 | local redis, err = require("resty.redis.connector").new({ 18 | url = "redis://PASSWORD@127.0.0.1:6379/2", 19 | }):connect() 20 | ``` 21 | 22 | More verbose configuration, with timeouts and a default password: 23 | 24 | ```lua 25 | local rc = require("resty.redis.connector").new({ 26 | connect_timeout = 50, 27 | send_timeout = 5000, 28 | read_timeout = 5000, 29 | keepalive_timeout = 30000, 30 | password = "mypass", 31 | }) 32 | 33 | local redis, err = rc:connect({ 34 | url = "redis://127.0.0.1:6379/2", 35 | }) 36 | 37 | -- ... 38 | 39 | local ok, err = rc:set_keepalive(redis) -- uses keepalive params 40 | ``` 41 | 42 | Keep all config in a table, to easily create / close connections as needed: 43 | 44 | ```lua 45 | local rc = require("resty.redis.connector").new({ 46 | connect_timeout = 50, 47 | send_timeout = 5000, 48 | read_timeout = 5000, 49 | keepalive_timeout = 30000, 50 | 51 | host = "127.0.0.1", 52 | port = 6379, 53 | db = 2, 54 | password = "mypass", 55 | }) 56 | 57 | local redis, err = rc:connect() 58 | 59 | -- ... 60 | 61 | local ok, err = rc:set_keepalive(redis) 62 | ``` 63 | 64 | [connect](#connect) can be used to override some defaults given in [new](#new), 65 | which are pertinent to this connection only. 66 | 67 | 68 | ```lua 69 | local rc = require("resty.redis.connector").new({ 70 | host = "127.0.0.1", 71 | port = 6379, 72 | db = 2, 73 | }) 74 | 75 | local redis, err = rc:connect({ 76 | db = 5, 77 | }) 78 | ``` 79 | 80 | 81 | ## DSN format 82 | 83 | If the `params.url` field is present then it will be parsed to set the other 84 | params. Any manually specified params will override values given in the DSN. 85 | 86 | *Note: this is a behaviour change as of v0.06. Previously, the DSN values would 87 | take precedence.* 88 | 89 | ### Direct Redis connections 90 | 91 | The format for connecting directly to Redis is: 92 | 93 | `redis://USERNAME:PASSWORD@HOST:PORT/DB` 94 | 95 | The `USERNAME`, `PASSWORD` and `DB` fields are optional, all other components 96 | are required. 97 | 98 | Use of username requires Redis 6.0.0 or newer. 99 | 100 | ### Connections via Redis Sentinel 101 | 102 | When connecting via Redis Sentinel, the format is as follows: 103 | 104 | `sentinel://USERNAME:PASSWORD@MASTER_NAME:ROLE/DB` 105 | 106 | Again, `USERNAME`, `PASSWORD` and `DB` are optional. `ROLE` must be either `m` 107 | or `s` for master / slave respectively. 108 | 109 | On versions of Redis newer than 5.0.1, Sentinels can optionally require their 110 | own password. If enabled, provide this password in the `sentinel_password` 111 | parameter. On Redis 6.2.0 and newer you can pass username using 112 | `sentinel_username` parameter. 113 | 114 | A table of `sentinels` must also be supplied. e.g. 115 | 116 | ```lua 117 | local redis, err = rc:connect{ 118 | url = "sentinel://mymaster:a/2", 119 | sentinels = { 120 | { host = "127.0.0.1", port = 26379 }, 121 | }, 122 | sentinel_username = "default", 123 | sentinel_password = "password" 124 | } 125 | ``` 126 | 127 | ## Proxy Mode 128 | 129 | Enable the `connection_is_proxied` parameter if connecting to Redis through a 130 | proxy service (e.g. Twemproxy). These proxies generally only support a limited 131 | sub-set of Redis commands, those which do not require state and do not affect 132 | multiple keys. Databases and transactions are also not supported. 133 | 134 | Proxy mode will disable switching to a DB on connect. Unsupported commands 135 | (defaults to those not supported by Twemproxy) will return `nil, err` 136 | immediately rather than being sent to the proxy, which can result in dropped 137 | connections. 138 | 139 | `discard` will not be sent when adding connections to the keepalive pool 140 | 141 | 142 | ## Disabled commands 143 | 144 | If configured as a table of commands, the command methods will be replaced by a 145 | function which immediately returns `nil, err` without forwarding the command to 146 | the server 147 | 148 | ## Default Parameters 149 | 150 | 151 | ```lua 152 | { 153 | connect_timeout = 100, 154 | send_timeout = 1000, 155 | read_timeout = 1000, 156 | keepalive_timeout = 60000, 157 | keepalive_poolsize = 30, 158 | 159 | -- ssl, ssl_verify, server_name, pool, pool_size, backlog 160 | -- see: https://github.com/openresty/lua-resty-redis#connect 161 | connection_options = {}, 162 | 163 | host = "127.0.0.1", 164 | port = "6379", 165 | path = "", -- unix socket path, e.g. /tmp/redis.sock 166 | username = "", 167 | password = "", 168 | sentinel_username = "", 169 | sentinel_password = "", 170 | db = 0, 171 | 172 | master_name = "mymaster", 173 | role = "master", -- master | slave 174 | sentinels = {}, 175 | 176 | connection_is_proxied = false, 177 | 178 | disabled_commands = {}, 179 | } 180 | ``` 181 | 182 | 183 | ## API 184 | 185 | * [new](#new) 186 | * [connect](#connect) 187 | * [set_keepalive](#set_keepalive) 188 | * [Utilities](#utilities) 189 | * [connect_via_sentinel](#connect_via_sentinel) 190 | * [try_hosts](#try_hosts) 191 | * [connect_to_host](#connect_to_host) 192 | * [sentinel.get_master](#sentinelget_master) 193 | * [sentinel.get_slaves](#sentinelget_slaves) 194 | 195 | 196 | ### new 197 | 198 | `syntax: rc = redis_connector.new(params)` 199 | 200 | Creates the Redis Connector object, overriding default params with the ones given. 201 | In case of failures, returns `nil` and a string describing the error. 202 | 203 | 204 | ### connect 205 | 206 | `syntax: redis, err = rc:connect(params)` 207 | 208 | Attempts to create a connection, according to the `params` supplied, falling back 209 | to [defaults](#default-parameters) given in `new` or the predefined defaults. If 210 | a connection cannot be made, returns `nil` and a string describing the reason. 211 | 212 | Note that `params` given here do not change the connector's own configuration, 213 | and are only used to alter this particular connection operation. As such, the 214 | following parameters have no meaning when given in `connect`. 215 | 216 | * `keepalive_poolsize` 217 | * `keepalive_timeout` 218 | * `connection_is_proxied` 219 | * `disabled_commands` 220 | 221 | 222 | ### set_keepalive 223 | 224 | `syntax: ok, err = rc:set_keepalive(redis)` 225 | 226 | Attempts to place the given Redis connection on the keepalive pool, according to 227 | timeout and poolsize params given in `new` or the predefined defaults. 228 | 229 | This allows an application to release resources without having to keep track of 230 | application wide keepalive settings. 231 | 232 | Returns `1` or in the case of error, `nil` and a string describing the error. 233 | 234 | 235 | ## Utilities 236 | 237 | The following methods are not typically needed, but may be useful if a custom 238 | interface is required. 239 | 240 | 241 | ### connect_via_sentinel 242 | 243 | `syntax: redis, err = rc:connect_via_sentinel(params)` 244 | 245 | Returns a Redis connection by first accessing a sentinel as supplied by the 246 | `params.sentinels` table, and querying this with the `params.master_name` and 247 | `params.role`. 248 | 249 | 250 | ### try_hosts 251 | 252 | `syntax: redis, err = rc:try_hosts(hosts)` 253 | 254 | Tries the hosts supplied in order and returns the first successful connection. 255 | 256 | 257 | ### connect_to_host 258 | 259 | `syntax: redis, err = rc:connect_to_host(host)` 260 | 261 | Attempts to connect to the supplied `host`. 262 | 263 | 264 | ### sentinel.get_master 265 | 266 | `syntax: master, err = sentinel.get_master(sentinel, master_name)` 267 | 268 | Given a connected Sentinel instance and a master name, will return the current 269 | master Redis instance. 270 | 271 | 272 | ### sentinel.get_slaves 273 | 274 | `syntax: slaves, err = sentinel.get_slaves(sentinel, master_name)` 275 | 276 | Given a connected Sentinel instance and a master name, will return a list of 277 | registered slave Redis instances. 278 | 279 | 280 | # Author 281 | 282 | James Hurst 283 | 284 | 285 | # Licence 286 | 287 | This module is licensed under the 2-clause BSD license. 288 | 289 | Copyright (c) James Hurst 290 | 291 | All rights reserved. 292 | 293 | Redistribution and use in source and binary forms, with or without modification, 294 | are permitted provided that the following conditions are met: 295 | 296 | * Redistributions of source code must retain the above copyright notice, this 297 | list of conditions and the following disclaimer. 298 | 299 | * Redistributions in binary form must reproduce the above copyright notice, this 300 | list of conditions and the following disclaimer in the documentation and/or 301 | other materials provided with the distribution. 302 | 303 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 304 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 305 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 306 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 307 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 308 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 309 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 310 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 311 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 312 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 313 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-redis-connector 2 | abstract=Connection utilities for lua-resty-redis, making it easy and reliable to connect to Redis hosts, either directly or via Redis Sentinel. 3 | author=James Hurst 4 | is_original=yes 5 | license=2bsd 6 | lib_dir=lib 7 | doc_dir=lib 8 | repo_link=https://github.com/ledgetech/lua-resty-redis-connector 9 | main_module=lib/resty/redis/connector.lua 10 | -------------------------------------------------------------------------------- /lib/resty/redis/connector.lua: -------------------------------------------------------------------------------- 1 | local ipairs, pairs, pcall, error, tostring, type, next, setmetatable, getmetatable = 2 | ipairs, pairs, pcall, error, tostring, type, next, setmetatable, getmetatable 3 | 4 | local ngx_log = ngx.log 5 | local ngx_ERR = ngx.ERR 6 | local ngx_re_match = ngx.re.match 7 | local null = ngx.null 8 | 9 | local str_find = string.find 10 | local str_sub = string.sub 11 | local tbl_remove = table.remove 12 | local tbl_sort = table.sort 13 | local tbl_clone, tbl_new 14 | do 15 | local ok 16 | ok, tbl_new = pcall(require, "table.new") 17 | if not ok then 18 | tbl_new = function (narr, nrec) return {} end -- luacheck: ignore 212 19 | end 20 | ok, tbl_clone = pcall(require, "table.clone") 21 | if not ok then 22 | tbl_clone = function (src) 23 | local result = {} 24 | for i, v in ipairs(src) do 25 | result[i] = v 26 | end 27 | for k, v in pairs(src) do 28 | result[k] = v 29 | end 30 | 31 | return result 32 | end 33 | end 34 | end 35 | 36 | local redis = require("resty.redis") 37 | redis.add_commands("sentinel") 38 | 39 | local get_master = require("resty.redis.sentinel").get_master 40 | local get_slaves = require("resty.redis.sentinel").get_slaves 41 | 42 | 43 | -- A metatable which prevents undefined fields from being created / accessed 44 | local fixed_field_metatable = { 45 | __index = 46 | function(t, k) -- luacheck: ignore 212 47 | error("field " .. tostring(k) .. " does not exist", 3) 48 | end, 49 | __newindex = 50 | function(t, k, v) -- luacheck: ignore 212 51 | error("attempt to create new field " .. tostring(k), 3) 52 | end, 53 | } 54 | 55 | 56 | -- Returns a new table, recursively copied from the one given, retaining 57 | -- metatable assignment. 58 | -- 59 | -- @param table table to be copied 60 | -- @return table 61 | local function tbl_copy(orig) 62 | local orig_type = type(orig) 63 | local copy 64 | if orig_type == "table" then 65 | copy = {} 66 | for orig_key, orig_value in next, orig, nil do 67 | copy[tbl_copy(orig_key)] = tbl_copy(orig_value) 68 | end 69 | setmetatable(copy, tbl_copy(getmetatable(orig))) 70 | else -- number, string, boolean, etc 71 | copy = orig 72 | end 73 | return copy 74 | end 75 | 76 | 77 | -- Returns a new table, recursively copied from the combination of the given 78 | -- table `t1`, with any missing fields copied from `defaults`. 79 | -- 80 | -- If `defaults` is of type "fixed field" and `t1` contains a field name not 81 | -- present in the defults, an error will be thrown. 82 | -- 83 | -- @param table t1 84 | -- @param table defaults 85 | -- @return table a new table, recursively copied and merged 86 | local function tbl_copy_merge_defaults(t1, defaults) 87 | if t1 == nil then t1 = {} end 88 | if defaults == nil then defaults = {} end 89 | if type(t1) == "table" and type(defaults) == "table" then 90 | local copy = {} 91 | for t1_key, t1_value in next, t1, nil do 92 | copy[tbl_copy(t1_key)] = tbl_copy_merge_defaults( 93 | t1_value, tbl_copy(defaults[t1_key]) 94 | ) 95 | end 96 | for defaults_key, defaults_value in next, defaults, nil do 97 | if t1[defaults_key] == nil then 98 | copy[tbl_copy(defaults_key)] = tbl_copy(defaults_value) 99 | end 100 | end 101 | return copy 102 | else 103 | return t1 -- not a table 104 | end 105 | end 106 | 107 | 108 | local DEFAULTS = setmetatable({ 109 | connect_timeout = 100, 110 | read_timeout = 1000, 111 | send_timeout = 1000, 112 | connection_options = {}, -- pool, etc 113 | keepalive_timeout = 60000, 114 | keepalive_poolsize = 30, 115 | 116 | host = "127.0.0.1", 117 | port = 6379, 118 | path = "", -- /tmp/redis.sock 119 | username = "", 120 | password = "", 121 | sentinel_username = "", 122 | sentinel_password = "", 123 | db = 0, 124 | url = "", -- DSN url 125 | 126 | master_name = "mymaster", 127 | role = "master", -- master | slave 128 | sentinels = {}, 129 | 130 | -- Redis proxies typically don't support full Redis capabilities 131 | connection_is_proxied = false, 132 | 133 | disabled_commands = {}, 134 | 135 | }, fixed_field_metatable) 136 | 137 | 138 | -- This is the set of commands unsupported by Twemproxy 139 | local default_disabled_commands = { 140 | "migrate", "move", "object", "randomkey", "rename", "renamenx", "scan", 141 | "bitop", "msetnx", "blpop", "brpop", "brpoplpush", "psubscribe", "publish", 142 | "punsubscribe", "subscribe", "unsubscribe", "discard", "exec", "multi", 143 | "unwatch", "watch", "script", "auth", "echo", "select", "bgrewriteaof", 144 | "bgsave", "client", "config", "dbsize", "debug", "flushall", "flushdb", 145 | "info", "lastsave", "monitor", "save", "shutdown", "slaveof", "slowlog", 146 | "sync", "time" 147 | } 148 | 149 | 150 | local _M = { 151 | _VERSION = '0.11.0', 152 | } 153 | 154 | local mt = { __index = _M } 155 | 156 | 157 | local function parse_dsn(params) 158 | local url = params.url 159 | if url and url ~= "" then 160 | local url_pattern = [[^(?:(redis|sentinel)://)(?:([^@]*)@)?([^:/]+)(?::(\d+|[msa]+))/?(.*)$]] 161 | 162 | local m, err = ngx_re_match(url, url_pattern, "oj") 163 | if not m then 164 | return nil, "could not parse DSN: " .. tostring(err) 165 | end 166 | 167 | -- TODO: Support a 'protocol' for proxied Redis? 168 | local fields 169 | if m[1] == "redis" then 170 | fields = { "password", "host", "port", "db" } 171 | elseif m[1] == "sentinel" then 172 | fields = { "password", "master_name", "role", "db" } 173 | end 174 | 175 | -- username/password may not be present 176 | if #m < 5 then tbl_remove(fields, 1) end 177 | 178 | local roles = { m = "master", s = "slave" } 179 | 180 | local parsed_params = {} 181 | 182 | for i, v in ipairs(fields) do 183 | if v == "db" or v == "port" then 184 | parsed_params[v] = tonumber(m[i + 1]) 185 | else 186 | parsed_params[v] = m[i + 1] 187 | end 188 | 189 | if v == "role" then 190 | parsed_params[v] = roles[parsed_params[v]] 191 | end 192 | end 193 | 194 | local colon_pos = str_find(parsed_params.password or "", ":", 1, true) 195 | if colon_pos then 196 | parsed_params.username = str_sub(parsed_params.password, 1, colon_pos - 1) 197 | parsed_params.password = str_sub(parsed_params.password, colon_pos + 1) 198 | end 199 | 200 | return tbl_copy_merge_defaults(params, parsed_params) 201 | end 202 | 203 | return params 204 | end 205 | _M.parse_dsn = parse_dsn 206 | 207 | 208 | -- Fill out gaps in config with any dsn params 209 | local function apply_dsn(config) 210 | if config and config.url then 211 | local err 212 | config, err = parse_dsn(config) 213 | if err then ngx_log(ngx_ERR, err) end 214 | end 215 | return config 216 | end 217 | 218 | 219 | -- For backwards compatability; previously send_timeout was implicitly the 220 | -- same as read_timeout. So if only the latter is given, ensure the former 221 | -- matches. 222 | local function apply_fallback_send_timeout(config) 223 | if config and not config.send_timeout and config.read_timeout then 224 | config.send_timeout = config.read_timeout 225 | end 226 | end 227 | 228 | 229 | function _M.new(config) 230 | config = apply_dsn(config) 231 | apply_fallback_send_timeout(config) 232 | 233 | local ok, config = pcall(tbl_copy_merge_defaults, config, DEFAULTS) 234 | if not ok then 235 | return nil, config -- err 236 | else 237 | -- In proxied Redis mode disable default commands 238 | if config.connection_is_proxied == true and 239 | not next(config.disabled_commands) then 240 | 241 | config.disabled_commands = default_disabled_commands 242 | end 243 | 244 | return setmetatable({ 245 | config = setmetatable(config, fixed_field_metatable) 246 | }, mt) 247 | end 248 | end 249 | 250 | 251 | function _M.connect(self, params) 252 | params = apply_dsn(params) 253 | apply_fallback_send_timeout(params) 254 | 255 | params = tbl_copy_merge_defaults(params, self.config) 256 | 257 | if #params.sentinels > 0 then 258 | return self:connect_via_sentinel(params) 259 | else 260 | return self:connect_to_host(params) 261 | end 262 | end 263 | 264 | 265 | local function sort_by_localhost(a, b) 266 | if a.host == "127.0.0.1" and b.host ~= "127.0.0.1" then 267 | return true 268 | else 269 | return false 270 | end 271 | end 272 | 273 | 274 | function _M.connect_via_sentinel(self, params) 275 | local master_name = params.master_name 276 | local role = params.role 277 | local db = params.db 278 | local username = params.username 279 | local password = params.password 280 | 281 | local sentinels = tbl_new(#params.sentinels, 0) 282 | for i, sentinel in ipairs(params.sentinels) do 283 | local host = tbl_clone(sentinel) 284 | host.db = null 285 | host.username = host.username or params.sentinel_username 286 | host.password = host.password or params.sentinel_password 287 | sentinels[i] = host 288 | end 289 | 290 | local sentnl, err, previous_errors = self:try_hosts(sentinels) 291 | if not sentnl then 292 | return nil, err, previous_errors 293 | end 294 | 295 | if role == "master" then 296 | local master, err = get_master(sentnl, master_name) 297 | if not master then 298 | return nil, err 299 | end 300 | 301 | sentnl:set_keepalive() 302 | 303 | master.db = db 304 | master.username = username 305 | master.password = password 306 | 307 | local redis, err = self:connect_to_host(master) 308 | if not redis then 309 | return nil, err 310 | end 311 | 312 | return redis 313 | else 314 | -- We want a slave 315 | local slaves, err = get_slaves(sentnl, master_name) 316 | if not slaves then 317 | return nil, err 318 | end 319 | 320 | sentnl:set_keepalive() 321 | 322 | -- Put any slaves on 127.0.0.1 at the front 323 | tbl_sort(slaves, sort_by_localhost) 324 | 325 | if db or password then 326 | for _, slave in ipairs(slaves) do 327 | slave.db = db 328 | slave.username = username 329 | slave.password = password 330 | end 331 | end 332 | 333 | local slave, err, previous_errors = self:try_hosts(slaves) 334 | if not slave then 335 | return nil, err, previous_errors 336 | end 337 | 338 | return slave 339 | end 340 | end 341 | 342 | 343 | -- In case of errors, returns "nil, err, previous_errors" where err is 344 | -- the last error received, and previous_errors is a table of the previous errors. 345 | function _M.try_hosts(self, hosts) 346 | local errors = tbl_new(#hosts, 0) 347 | 348 | for i, host in ipairs(hosts) do 349 | local r, err = self:connect_to_host(host) 350 | if r and not err then 351 | return r, nil, errors 352 | else 353 | errors[i] = err 354 | end 355 | end 356 | 357 | return nil, "no hosts available", errors 358 | end 359 | 360 | 361 | function _M.connect_to_host(self, host) 362 | local r = redis.new() 363 | 364 | -- config options in 'host' should override the global defaults 365 | -- host contains keys that aren't in config 366 | -- this can break tbl_copy_merge_defaults, hence the manual loop here 367 | local config = tbl_copy(self.config) 368 | for k, _ in pairs(config) do 369 | if host[k] then 370 | config[k] = host[k] 371 | end 372 | end 373 | 374 | r:set_timeouts( 375 | config.connect_timeout, 376 | config.send_timeout, 377 | config.read_timeout 378 | ) 379 | 380 | -- Stub out methods for disabled commands 381 | if next(config.disabled_commands) then 382 | for _, cmd in ipairs(config.disabled_commands) do 383 | r[cmd] = function() 384 | return nil, ("Command "..cmd.." is disabled") 385 | end 386 | end 387 | end 388 | 389 | local ok, err 390 | local path = host.path 391 | local opts = config.connection_options 392 | if path and path ~= "" then 393 | if opts then 394 | ok, err = r:connect(path, config.connection_options) 395 | else 396 | ok, err = r:connect(path) 397 | end 398 | else 399 | if opts then 400 | ok, err = r:connect(host.host, host.port, config.connection_options) 401 | else 402 | ok, err = r:connect(host.host, host.port) 403 | end 404 | end 405 | 406 | if not ok then 407 | return nil, err 408 | else 409 | local username = host.username 410 | local password = host.password 411 | if password and password ~= "" then 412 | local res 413 | -- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary 414 | if username and username ~= "" and username ~= "default" then 415 | res, err = r:auth(username, password) 416 | else 417 | res, err = r:auth(password) 418 | end 419 | if err then 420 | return res, err 421 | end 422 | end 423 | 424 | -- No support for DBs in proxied Redis and Redis Sentinel 425 | if config.connection_is_proxied ~= true and host.db ~= nil and host.db ~= null then 426 | local res, select_err = r:select(host.db) 427 | 428 | -- SELECT will fail if we are connected to sentinel: 429 | -- detect it and ignore error message it that's the case 430 | if select_err and str_find(select_err, "ERR unknown command") then 431 | local role = r:role() 432 | if role and role[1] == "sentinel" then 433 | select_err = nil 434 | end 435 | end 436 | if select_err then 437 | return res, select_err 438 | end 439 | end 440 | return r, nil 441 | end 442 | end 443 | 444 | 445 | function _M.set_keepalive(self, redis) 446 | -- Restore connection to "NORMAL" before putting into keepalive pool, 447 | -- ignoring any errors. 448 | -- Proxied Redis does not support transactions. 449 | if self.config.connection_is_proxied ~= true then 450 | redis:discard() 451 | end 452 | 453 | local config = self.config 454 | return redis:set_keepalive( 455 | config.keepalive_timeout, config.keepalive_poolsize 456 | ) 457 | end 458 | 459 | 460 | -- Deprecated: use config table in new() or connect() instead. 461 | function _M.set_connect_timeout(self, timeout) 462 | self.config.connect_timeout = timeout 463 | end 464 | 465 | 466 | -- Deprecated: use config table in new() or connect() instead. 467 | function _M.set_read_timeout(self, timeout) 468 | self.config.read_timeout = timeout 469 | end 470 | 471 | 472 | -- Deprecated: use config table in new() or connect() instead. 473 | function _M.set_connection_options(self, options) 474 | self.config.connection_options = options 475 | end 476 | 477 | 478 | return setmetatable(_M, fixed_field_metatable) 479 | -------------------------------------------------------------------------------- /lib/resty/redis/sentinel.lua: -------------------------------------------------------------------------------- 1 | local ipairs, type = ipairs, type 2 | 3 | local ngx_null = ngx.null 4 | 5 | local tbl_insert = table.insert 6 | local ok, tbl_new = pcall(require, "table.new") 7 | if not ok then 8 | tbl_new = function (narr, nrec) return {} end -- luacheck: ignore 212 9 | end 10 | 11 | 12 | local _M = { 13 | _VERSION = '0.11.0' 14 | } 15 | 16 | 17 | function _M.get_master(sentinel, master_name) 18 | local res, err = sentinel:sentinel( 19 | "get-master-addr-by-name", 20 | master_name 21 | ) 22 | if res and res ~= ngx_null and res[1] and res[2] then 23 | return { host = res[1], port = res[2] } 24 | elseif res == ngx_null then 25 | return nil, "invalid master name" 26 | else 27 | return nil, err 28 | end 29 | end 30 | 31 | 32 | function _M.get_slaves(sentinel, master_name) 33 | local res, err = sentinel:sentinel("slaves", master_name) 34 | 35 | if res and type(res) == "table" then 36 | local hosts = tbl_new(#res, 0) 37 | for _,slave in ipairs(res) do 38 | local num_recs = #slave 39 | local host = tbl_new(0, num_recs + 1) 40 | for i = 1, num_recs, 2 do 41 | host[slave[i]] = slave[i + 1] 42 | end 43 | 44 | local master_link_status_ok = host["master-link-status"] == "ok" 45 | local is_down = host["flags"] and (string.find(host["flags"],"s_down") 46 | or string.find(host["flags"],"disconnected")) 47 | if master_link_status_ok and not is_down then 48 | host.host = host.ip -- for parity with other functions 49 | tbl_insert(hosts, host) 50 | end 51 | end 52 | if hosts[1] ~= nil then 53 | return hosts 54 | else 55 | return nil, "no slaves available" 56 | end 57 | else 58 | return nil, err 59 | end 60 | end 61 | 62 | 63 | return _M 64 | -------------------------------------------------------------------------------- /lua-resty-redis-connector-0.11.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-redis-connector" 2 | version = "0.11.0-0" 3 | source = { 4 | url = "git://github.com/ledgetech/lua-resty-redis-connector", 5 | tag = "v0.11.0" 6 | } 7 | description = { 8 | summary = "Connection utilities for lua-resty-redis.", 9 | detailed = [[ 10 | Connection utilities for lua-resty-redis, making it easy and 11 | reliable to connect to Redis hosts, either directly or via Redis 12 | Sentinel. 13 | ]], 14 | homepage = "https://github.com/ledgetech/lua-resty-redis-connector", 15 | license = "2-clause BSD", 16 | maintainer = "James Hurst " 17 | } 18 | dependencies = { 19 | "lua >= 5.1", 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | ["resty.redis.connector"] = "lib/resty/redis/connector.lua", 25 | ["resty.redis.sentinel"] = "lib/resty/redis/sentinel.lua" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /t/config.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket::Lua; 2 | use Cwd qw(cwd); 3 | 4 | repeat_each(2); 5 | 6 | plan tests => repeat_each() * blocks() * 2; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | lua_socket_log_errors Off; 13 | 14 | init_by_lua_block { 15 | require("luacov.runner").init() 16 | } 17 | }; 18 | 19 | $ENV{TEST_NGINX_REDIS_PORT} ||= 6380; 20 | 21 | no_long_string(); 22 | run_tests(); 23 | 24 | __DATA__ 25 | 26 | === TEST 1: Default config 27 | --- http_config eval: $::HttpConfig 28 | --- config 29 | location /t { 30 | content_by_lua_block { 31 | local rc = assert(require("resty.redis.connector").new()) 32 | } 33 | } 34 | --- request 35 | GET /t 36 | --- no_error_log 37 | [error] 38 | 39 | 40 | === TEST 2: Defaults via new 41 | --- http_config eval: $::HttpConfig 42 | --- config 43 | location /t { 44 | content_by_lua_block { 45 | local config = { 46 | connect_timeout = 500, 47 | port = $TEST_NGINX_REDIS_PORT, 48 | db = 6, 49 | } 50 | local rc = require("resty.redis.connector").new(config) 51 | 52 | assert(config ~= rc.config, "config should not equal rc.config") 53 | assert(rc.config.connect_timeout == 500, "connect_timeout should be 500") 54 | assert(rc.config.db == 6, "db should be 6") 55 | assert(rc.config.role == "master", "role should be master") 56 | } 57 | } 58 | --- request 59 | GET /t 60 | --- no_error_log 61 | [error] 62 | 63 | 64 | === TEST 3: Config via connect still overrides 65 | --- http_config eval: $::HttpConfig 66 | --- config 67 | location /t { 68 | content_by_lua_block { 69 | local rc = require("resty.redis.connector").new({ 70 | connect_timeout = 500, 71 | port = $TEST_NGINX_REDIS_PORT, 72 | db = 6, 73 | keepalive_poolsize = 10, 74 | }) 75 | 76 | assert(config ~= rc.config, "config should not equal rc.config") 77 | assert(rc.config.connect_timeout == 500, "connect_timeout should be 500") 78 | assert(rc.config.db == 6, "db should be 6") 79 | assert(rc.config.role == "master", "role should be master") 80 | assert(rc.config.keepalive_poolsize == 10, 81 | "keepalive_poolsize should be 10") 82 | 83 | local redis, err = rc:connect({ 84 | port = $TEST_NGINX_REDIS_PORT, 85 | disabled_commands = { "set" } 86 | }) 87 | 88 | if not redis or err then 89 | ngx.log(ngx.ERR, "connect failed: ", err) 90 | return 91 | end 92 | 93 | local ok, err = redis:set("foo", "bar") 94 | assert( ok == nil and (string.find(err, "disabled") ~= nil) , "Disabled commands not passed through" ) 95 | } 96 | } 97 | --- request 98 | GET /t 99 | --- no_error_log 100 | [error] 101 | 102 | 103 | === TEST 4: Unknown config errors, all config does not error 104 | --- http_config eval: $::HttpConfig 105 | --- config 106 | location /t { 107 | content_by_lua_block { 108 | local rc, err = require("resty.redis.connector").new({ 109 | connect_timeout = 500, 110 | port = $TEST_NGINX_REDIS_PORT, 111 | db = 6, 112 | foo = "bar", 113 | }) 114 | 115 | assert(rc == nil, "rc should be nil") 116 | assert(string.find(err, "field foo does not exist"), 117 | "err should contain error") 118 | 119 | -- Provide all options, without errors 120 | 121 | assert(require("resty.redis.connector").new({ 122 | connect_timeout = 100, 123 | send_timeout = 500, 124 | read_timeout = 1000, 125 | connection_options = { pool = "::" }, 126 | keepalive_timeout = 60000, 127 | keepalive_poolsize = 30, 128 | 129 | host = "127.0.0.1", 130 | port = $TEST_NGINX_REDIS_PORT, 131 | path = "", 132 | username = "", 133 | password = "", 134 | db = 0, 135 | 136 | url = "", 137 | 138 | master_name = "mymaster", 139 | role = "master", 140 | sentinels = {}, 141 | }), "new should return positively") 142 | 143 | -- Provide all options via connect, without errors 144 | 145 | local rc = require("resty.redis.connector").new() 146 | 147 | assert(rc:connect({ 148 | connect_timeout = 100, 149 | send_timeout = 500, 150 | read_timeout = 1000, 151 | connection_options = { pool = "::" }, 152 | keepalive_timeout = 60000, 153 | keepalive_poolsize = 30, 154 | 155 | host = "127.0.0.1", 156 | port = $TEST_NGINX_REDIS_PORT, 157 | path = "", 158 | username = "", 159 | password = "", 160 | db = 0, 161 | 162 | url = "", 163 | 164 | master_name = "mymaster", 165 | role = "master", 166 | sentinels = {}, 167 | }), "rc:connect should return positively") 168 | } 169 | } 170 | --- request 171 | GET /t 172 | --- no_error_log 173 | [error] 174 | 175 | 176 | === TEST 5: timeout defaults 177 | --- http_config eval: $::HttpConfig 178 | --- config 179 | location /t { 180 | content_by_lua_block { 181 | -- global defaults 182 | local rc = require("resty.redis.connector").new({ 183 | port = $TEST_NGINX_REDIS_PORT, 184 | db = 6, 185 | keepalive_poolsize = 10, 186 | }) 187 | 188 | assert(rc.config.connect_timeout == 100, "connect_timeout should be 100") 189 | assert(rc.config.send_timeout == 1000, "send_timeout should be 1000") 190 | assert(rc.config.read_timeout == 1000, "read_timeout should be 1000") 191 | 192 | local redis = assert(rc:connect(), "rc:connect should return positively") 193 | assert(redis:set("foo", "bar")) 194 | rc:set_keepalive(redis) 195 | 196 | -- send_timeout defaults to read_timeout 197 | rc = require("resty.redis.connector").new({ 198 | read_timeout = 500, 199 | port = $TEST_NGINX_REDIS_PORT, 200 | db = 6, 201 | keepalive_poolsize = 10, 202 | }) 203 | 204 | assert(rc.config.connect_timeout == 100, "connect_timeout should be 100") 205 | assert(rc.config.send_timeout == 500, "send_timeout should be 500") 206 | assert(rc.config.read_timeout == 500, "read_timeout should be 500") 207 | 208 | local redis = assert(rc:connect(), "rc:connect should return positively") 209 | assert(redis:set("foo", "bar")) 210 | rc:set_keepalive(redis) 211 | 212 | -- send_timeout can be set separately from read_timeout 213 | rc = require("resty.redis.connector").new({ 214 | send_timeout = 500, 215 | read_timeout = 200, 216 | port = $TEST_NGINX_REDIS_PORT, 217 | db = 6, 218 | keepalive_poolsize = 10, 219 | }) 220 | 221 | assert(rc.config.connect_timeout == 100, "connect_timeout should be 100") 222 | assert(rc.config.send_timeout == 500, "send_timeout should be 500") 223 | assert(rc.config.read_timeout == 200, "read_timeout should be 200") 224 | } 225 | } 226 | --- request 227 | GET /t 228 | --- no_error_log 229 | [error] 230 | -------------------------------------------------------------------------------- /t/connector.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | our $HttpConfig = qq{ 7 | lua_package_path "$pwd/lib/?.lua;;"; 8 | lua_socket_log_errors Off; 9 | 10 | init_by_lua_block { 11 | require("luacov.runner").init() 12 | } 13 | }; 14 | 15 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 16 | $ENV{TEST_NGINX_REDIS_PORT} ||= 6380; 17 | $ENV{TEST_NGINX_REDIS_PORT_AUTH} ||= 6393; 18 | $ENV{TEST_NGINX_REDIS_SOCKET} ||= 'unix://tmp/redis/redis.sock'; 19 | 20 | no_long_string(); 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: basic connect 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location /t { 29 | content_by_lua_block { 30 | local rc = require("resty.redis.connector").new({ 31 | port = $TEST_NGINX_REDIS_PORT 32 | }) 33 | 34 | local redis, err = assert(rc:connect(params), 35 | "connect should return positively") 36 | 37 | assert(redis:set("dog", "an animal"), 38 | "redis:set should return positively") 39 | 40 | redis:close() 41 | } 42 | } 43 | --- request 44 | GET /t 45 | --- no_error_log 46 | [error] 47 | 48 | 49 | === TEST 2: try_hosts 50 | --- http_config eval: $::HttpConfig 51 | --- config 52 | location /t { 53 | lua_socket_log_errors off; 54 | content_by_lua_block { 55 | local rc = require("resty.redis.connector").new({ 56 | connect_timeout = 100, 57 | }) 58 | 59 | local hosts = { 60 | { host = "127.0.0.1", port = 1 }, 61 | { host = "127.0.0.1", port = 2 }, 62 | { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT }, 63 | } 64 | 65 | local redis, err, previous_errors = rc:try_hosts(hosts) 66 | assert(redis and not err, 67 | "try_hosts should return a connection and no error") 68 | 69 | assert(string.len(previous_errors[1]) > 0, 70 | "previous_errors[1] should contain an error") 71 | assert(string.len(previous_errors[2]) > 0, 72 | "previous_errors[2] should contain an error") 73 | 74 | assert(redis:set("dog", "an animal"), 75 | "redis connection should be working") 76 | 77 | redis:close() 78 | 79 | local hosts = { 80 | { host = "127.0.0.1", port = 1 }, 81 | { host = "127.0.0.1", port = 2 }, 82 | } 83 | 84 | local redis, err, previous_errors = rc:try_hosts(hosts) 85 | assert(not redis and err == "no hosts available", 86 | "no available hosts should return an error") 87 | 88 | assert(string.len(previous_errors[1]) > 0, 89 | "previous_errors[1] should contain an error") 90 | assert(string.len(previous_errors[2]) > 0, 91 | "previous_errors[2] should contain an error") 92 | } 93 | } 94 | --- request 95 | GET /t 96 | --- no_error_log 97 | [error] 98 | 99 | 100 | === TEST 3: connect_to_host 101 | --- http_config eval: $::HttpConfig 102 | --- config 103 | location /t { 104 | content_by_lua_block { 105 | local rc = require("resty.redis.connector").new() 106 | 107 | local host = { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT } 108 | 109 | local redis, err = rc:connect_to_host(host) 110 | assert(redis and not err, 111 | "connect_to_host should return positively") 112 | 113 | assert(redis:set("dog", "an animal"), 114 | "redis connection should be working") 115 | 116 | redis:close() 117 | } 118 | } 119 | --- request 120 | GET /t 121 | --- no_error_log 122 | [error] 123 | 124 | 125 | === TEST 4: connect_to_host options ignore defaults 126 | --- http_config eval: $::HttpConfig 127 | --- config 128 | location /t { 129 | content_by_lua_block { 130 | local rc = require("resty.redis.connector").new({ 131 | port = $TEST_NGINX_REDIS_PORT, 132 | db = 2, 133 | }) 134 | 135 | local redis, err = assert(rc:connect_to_host({ 136 | host = "127.0.0.1", 137 | db = 1, 138 | port = $TEST_NGINX_REDIS_PORT 139 | }), "connect_to_host should return positively") 140 | 141 | assert(redis:set("dog", "an animal") == "OK", 142 | "set should return 'OK'") 143 | 144 | redis:select(2) 145 | assert(redis:get("dog") == ngx.null, 146 | "dog should not exist in db 2") 147 | 148 | redis:select(1) 149 | assert(redis:get("dog") == "an animal", 150 | "dog should be 'an animal' in db 1") 151 | 152 | redis:close() 153 | } 154 | } 155 | --- request 156 | GET /t 157 | --- no_error_log 158 | [error] 159 | 160 | 161 | === TEST 5: Test set_keepalive method 162 | --- http_config eval: $::HttpConfig 163 | --- config 164 | location /t { 165 | lua_socket_log_errors Off; 166 | content_by_lua_block { 167 | local rc = require("resty.redis.connector").new({ 168 | port = $TEST_NGINX_REDIS_PORT, 169 | }) 170 | 171 | local redis = assert(rc:connect(), 172 | "rc:connect should return positively") 173 | local ok, err = rc:set_keepalive(redis) 174 | assert(not err, "set_keepalive error should be nil") 175 | 176 | local ok, err = redis:set("foo", "bar") 177 | assert(not ok, "ok should be nil") 178 | assert(string.find(err, "closed"), "error should contain 'closed'") 179 | 180 | local redis = assert(rc:connect(), "connect should return positively") 181 | assert(redis:subscribe("channel"), "subscribe should return positively") 182 | 183 | local ok, err = rc:set_keepalive(redis) 184 | assert(not ok, "ok should be nil") 185 | assert(string.find(err, "subscribed state"), 186 | "error should contain 'subscribed state'") 187 | 188 | } 189 | } 190 | --- request 191 | GET /t 192 | --- no_error_log 193 | [error] 194 | 195 | 196 | === TEST 6: password 197 | --- http_config eval: $::HttpConfig 198 | --- config 199 | location /t { 200 | lua_socket_log_errors Off; 201 | content_by_lua_block { 202 | local rc = require("resty.redis.connector").new({ 203 | port = $TEST_NGINX_REDIS_PORT, 204 | password = "foo", 205 | }) 206 | 207 | local redis, err = rc:connect() 208 | assert(not redis and string.find(err, "ERR") and string.find(err, "AUTH"), 209 | "connect should fail with password error") 210 | 211 | } 212 | } 213 | --- request 214 | GET /t 215 | --- no_error_log 216 | [error] 217 | 218 | === TEST 7: username and password 219 | --- http_config eval: $::HttpConfig 220 | --- config 221 | location /t { 222 | lua_socket_log_errors Off; 223 | content_by_lua_block { 224 | local rc = require("resty.redis.connector").new({ 225 | port = $TEST_NGINX_REDIS_PORT, 226 | username = "x", 227 | password = "foo", 228 | }) 229 | 230 | local redis, err = rc:connect() 231 | assert(not redis and string.find(err, "WRONGPASS"), 232 | "connect should fail with invalid username-password error") 233 | } 234 | } 235 | --- request 236 | GET /t 237 | --- no_error_log 238 | [error] 239 | 240 | 241 | === TEST 8: Bad unix domain socket path should fail 242 | --- http_config eval: $::HttpConfig 243 | --- config 244 | location /t { 245 | lua_socket_log_errors Off; 246 | content_by_lua_block { 247 | local redis, err = require("resty.redis.connector").new({ 248 | path = "unix://GARBAGE_PATH_AKFDKAJSFKJSAFLKJSADFLKJSANCKAJSNCKJSANCLKAJS", 249 | }):connect() 250 | 251 | assert(not redis and err == "no such file or directory", 252 | "bad domain socket should fail") 253 | } 254 | } 255 | --- request 256 | GET /t 257 | --- no_error_log 258 | [error] 259 | 260 | 261 | === TEST 8.1: Good unix domain socket path should succeed 262 | --- http_config eval: $::HttpConfig 263 | --- config 264 | location /t { 265 | lua_socket_log_errors Off; 266 | content_by_lua_block { 267 | local redis, err = require("resty.redis.connector").new({ 268 | path = "$TEST_NGINX_REDIS_SOCKET", 269 | }):connect() 270 | 271 | assert (redis and not err, 272 | "connection should be valid") 273 | 274 | redis:close() 275 | } 276 | } 277 | --- request 278 | GET /t 279 | --- no_error_log 280 | [error] 281 | 282 | 283 | === TEST 9: parse_dsn 284 | --- http_config eval: $::HttpConfig 285 | --- config 286 | location /t { 287 | lua_socket_log_errors Off; 288 | content_by_lua_block { 289 | local rc = require("resty.redis.connector") 290 | 291 | local user_params = { 292 | url = "redis://foo@127.0.0.1:$TEST_NGINX_REDIS_PORT/4" 293 | } 294 | 295 | local params, err = rc.parse_dsn(user_params) 296 | assert(params and not err, 297 | "url should parse without error: " .. tostring(err)) 298 | 299 | assert(params.host == "127.0.0.1", "host should be localhost") 300 | assert(tonumber(params.port) == $TEST_NGINX_REDIS_PORT, 301 | "port should be $TEST_NGINX_REDIS_PORT") 302 | assert(tonumber(params.db) == 4, "db should be 4") 303 | assert(params.password == "foo", "password should be foo") 304 | 305 | 306 | local user_params = { 307 | url = "sentinel://foo:bar@foomaster:s/2" 308 | } 309 | 310 | local params, err = rc.parse_dsn(user_params) 311 | assert(params and not err, 312 | "url should parse without error: " .. tostring(err)) 313 | 314 | assert(params.master_name == "foomaster", "master_name should be foomaster") 315 | assert(params.role == "slave", "role should be slave") 316 | assert(tonumber(params.db) == 2, "db should be 2") 317 | assert(params.username == "foo", "username should be foo") 318 | assert(params.password == "bar", "password should be bar") 319 | 320 | 321 | local params = { 322 | url = "sentinels:/wrongformat", 323 | } 324 | 325 | local ok, err = rc.parse_dsn(params) 326 | assert(not ok and err == "could not parse DSN: nil", 327 | "url should fail to parse") 328 | } 329 | } 330 | --- request 331 | GET /t 332 | --- no_error_log 333 | [error] 334 | 335 | 336 | === TEST 10: params override dsn components 337 | --- http_config eval: $::HttpConfig 338 | --- config 339 | location /t { 340 | lua_socket_log_errors Off; 341 | content_by_lua_block { 342 | local rc = require("resty.redis.connector") 343 | 344 | local user_params = { 345 | url = "redis://foo@127.0.0.1:$TEST_NGINX_REDIS_PORT/4", 346 | db = 2, 347 | password = "bar", 348 | host = "example.com", 349 | } 350 | 351 | local params, err = rc.parse_dsn(user_params) 352 | assert(params and not err, 353 | "url should parse without error: " .. tostring(err)) 354 | 355 | assert(tonumber(params.db) == 2, "db should be 2") 356 | assert(params.password == "bar", "password should be bar") 357 | assert(params.host == "example.com", "host should be example.com") 358 | 359 | assert(tonumber(params.port) == $TEST_NGINX_REDIS_PORT, "port should still be $TEST_NGINX_REDIS_PORT") 360 | 361 | } 362 | } 363 | --- request 364 | GET /t 365 | --- no_error_log 366 | [error] 367 | 368 | 369 | === TEST 11: Integration test for parse_dsn 370 | --- http_config eval: $::HttpConfig 371 | --- config 372 | location /t { 373 | lua_socket_log_errors Off; 374 | content_by_lua_block { 375 | local user_params = { 376 | url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", 377 | db = 2, 378 | host = "127.0.0.1", 379 | } 380 | 381 | local rc, err = require("resty.redis.connector").new(user_params) 382 | assert(rc and not err, "new should return positively") 383 | 384 | local redis, err = rc:connect() 385 | assert(redis and not err, "connect should return positively") 386 | assert(redis:set("cat", "dog") and redis:get("cat") == "dog") 387 | 388 | local redis, err = rc:connect({ 389 | url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", 390 | db = 2, 391 | host = "127.0.0.1", 392 | }) 393 | assert(redis and not err, "connect should return positively") 394 | assert(redis:set("cat", "dog") and redis:get("cat") == "dog") 395 | 396 | 397 | local rc2, err = require("resty.redis.connector").new() 398 | local redis, err = rc2:connect({ 399 | url = "redis://foo.example:$TEST_NGINX_REDIS_PORT/4", 400 | db = 2, 401 | host = "127.0.0.1", 402 | }) 403 | assert(redis and not err, "connect should return positively") 404 | assert(redis:set("cat", "dog") and redis:get("cat") == "dog") 405 | 406 | local redis, err = rc2:connect({ 407 | url = "redis://redisuser:redisuserpass@127.0.0.1:$TEST_NGINX_REDIS_PORT_AUTH/" 408 | }) 409 | assert(redis and not err, "connect should return positively") 410 | local username = assert(redis:acl("whoami")) 411 | assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username)) 412 | } 413 | } 414 | --- request 415 | GET /t 416 | --- no_error_log 417 | [error] 418 | 419 | 420 | === TEST 12: DSN without DB 421 | --- http_config eval: $::HttpConfig 422 | --- config 423 | location /t { 424 | lua_socket_log_errors Off; 425 | content_by_lua_block { 426 | local user_params = { 427 | url = "redis://foo.example:$TEST_NGINX_REDIS_PORT", 428 | host = "127.0.0.1", 429 | } 430 | 431 | local rc, err = require("resty.redis.connector").new(user_params) 432 | assert(rc and not err, "new should return positively") 433 | 434 | local redis, err = rc:connect() 435 | assert(redis and not err, "connect should return positively") 436 | assert(redis:set("cat", "dog") and redis:get("cat") == "dog") 437 | } 438 | } 439 | --- request 440 | GET /t 441 | --- no_error_log 442 | [error] 443 | -------------------------------------------------------------------------------- /t/proxy.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | our $HttpConfig = qq{ 7 | lua_package_path "$pwd/lib/?.lua;;"; 8 | lua_socket_log_errors Off; 9 | 10 | init_by_lua_block { 11 | require("luacov.runner").init() 12 | } 13 | }; 14 | 15 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 16 | $ENV{TEST_NGINX_REDIS_PORT} ||= 6380; 17 | 18 | no_long_string(); 19 | run_tests(); 20 | 21 | __DATA__ 22 | 23 | === TEST 1: Proxy mode disables commands 24 | --- http_config eval: $::HttpConfig 25 | --- config 26 | location /t { 27 | content_by_lua_block { 28 | local rc = require("resty.redis.connector").new({ 29 | port = $TEST_NGINX_REDIS_PORT, 30 | connection_is_proxied = true 31 | }) 32 | 33 | local redis, err = assert(rc:connect(params), 34 | "connect should return positively") 35 | 36 | assert(redis:set("dog", "an animal"), 37 | "redis:set should return positively") 38 | 39 | local ok, err = redis:multi() 40 | assert(ok == nil, "redis:multi should return nil") 41 | assert(err == "Command multi is disabled") 42 | 43 | redis:close() 44 | } 45 | } 46 | --- request 47 | GET /t 48 | --- no_error_log 49 | [error] 50 | 51 | 52 | === TEST 2: Proxy mode disables custom commands 53 | --- http_config eval: $::HttpConfig 54 | --- config 55 | location /t { 56 | content_by_lua_block { 57 | local rc = require("resty.redis.connector").new({ 58 | port = $TEST_NGINX_REDIS_PORT, 59 | connection_is_proxied = true, 60 | disabled_commands = { "foobar", "hget"} 61 | }) 62 | 63 | local redis, err = assert(rc:connect(params), 64 | "connect should return positively") 65 | 66 | assert(redis:set("dog", "an animal"), 67 | "redis:set should return positively") 68 | 69 | assert(redis:multi(), 70 | "redis:multi should return positively") 71 | 72 | local ok, err = redis:hget() 73 | assert(ok == nil, "redis:hget should return nil") 74 | assert(err == "Command hget is disabled") 75 | 76 | local ok, err = redis:foobar() 77 | assert(ok == nil, "redis:foobar should return nil") 78 | assert(err == "Command foobar is disabled") 79 | 80 | redis:close() 81 | } 82 | } 83 | --- request 84 | GET /t 85 | --- no_error_log 86 | [error] 87 | 88 | === TEST 3: Proxy mode does not switch DB 89 | --- http_config eval: $::HttpConfig 90 | --- config 91 | location /t { 92 | content_by_lua_block { 93 | local redis = require("resty.redis.connector").new({ 94 | port = $TEST_NGINX_REDIS_PORT, 95 | db = 2 96 | }):connect() 97 | 98 | local proxy = require("resty.redis.connector").new({ 99 | port = $TEST_NGINX_REDIS_PORT, 100 | connection_is_proxied = true, 101 | db = 2 102 | }):connect() 103 | 104 | assert(redis:set("proxy", "test"), 105 | "redis:set should return positively") 106 | 107 | assert(proxy:get("proxy") == ngx.null, 108 | "proxy key should not exist in proxy") 109 | 110 | redis:seelct(2) 111 | assert(redis:get("proxy") == "test", 112 | "proxy key should be 'test' in db 1") 113 | 114 | redis:close() 115 | } 116 | } 117 | --- request 118 | GET /t 119 | --- no_error_log 120 | [error] 121 | 122 | 123 | === TEST 4: Commands are disabled without proxy mode 124 | --- http_config eval: $::HttpConfig 125 | --- config 126 | location /t { 127 | content_by_lua_block { 128 | local rc = require("resty.redis.connector").new({ 129 | port = $TEST_NGINX_REDIS_PORT, 130 | disabled_commands = { "foobar", "hget"} 131 | }) 132 | 133 | local redis, err = assert(rc:connect(params), 134 | "connect should return positively") 135 | 136 | assert(redis:set("dog", "an animal"), 137 | "redis:set should return positively") 138 | 139 | assert(redis:multi(), 140 | "redis:multi should return positively") 141 | 142 | local ok, err = redis:hget() 143 | assert(ok == nil, "redis:hget should return nil") 144 | assert(err == "Command hget is disabled") 145 | 146 | local ok, err = redis:foobar() 147 | assert(ok == nil, "redis:foobar should return nil") 148 | assert(err == "Command foobar is disabled") 149 | 150 | redis:close() 151 | } 152 | } 153 | --- request 154 | GET /t 155 | --- no_error_log 156 | [error] 157 | -------------------------------------------------------------------------------- /t/sentinel.t: -------------------------------------------------------------------------------- 1 | use Test::Nginx::Socket 'no_plan'; 2 | use Cwd qw(cwd); 3 | 4 | my $pwd = cwd(); 5 | 6 | our $HttpConfig = qq{ 7 | lua_package_path "$pwd/lib/?.lua;;"; 8 | lua_socket_log_errors Off; 9 | 10 | init_by_lua_block { 11 | require("luacov.runner").init() 12 | } 13 | }; 14 | 15 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 16 | $ENV{TEST_NGINX_REDIS_PORT} ||= 6380; 17 | $ENV{TEST_NGINX_REDIS_PORT_SL1} ||= 6381; 18 | $ENV{TEST_NGINX_REDIS_PORT_SL2} ||= 6382; 19 | $ENV{TEST_NGINX_SENTINEL_PORT1} ||= 6390; 20 | $ENV{TEST_NGINX_SENTINEL_PORT2} ||= 6391; 21 | $ENV{TEST_NGINX_SENTINEL_PORT3} ||= 6392; 22 | $ENV{TEST_NGINX_SENTINEL_PORT_AUTH} ||= 6393; 23 | 24 | no_long_string(); 25 | run_tests(); 26 | 27 | __DATA__ 28 | 29 | === TEST 1: Connect using redis:// protocol 30 | --- http_config eval: $::HttpConfig 31 | --- config 32 | location /t { 33 | content_by_lua_block { 34 | local rc = require("resty.redis.connector").new() 35 | local sentinel, err, pong 36 | 37 | sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1", db = ngx.null } 38 | assert(sentinel and not err, "sentinel should connect without errors but got " .. tostring(err)) 39 | pong, err = sentinel:ping() 40 | assert(pong and not err, "sentinel should respond to PING got " .. tostring(err)) 41 | sentinel:close() 42 | 43 | sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1", db = 0 } 44 | assert(sentinel and not err, "sentinel should connect with db=0 without errors but got " .. tostring(err)) 45 | pong, err = sentinel:ping() 46 | assert(pong and not err, "sentinel should respond to PING got " .. tostring(err)) 47 | sentinel:close() 48 | } 49 | } 50 | --- request 51 | GET /t 52 | --- no_error_log 53 | [error] 54 | 55 | 56 | === TEST 2: Get the master 57 | --- http_config eval: $::HttpConfig 58 | --- config 59 | location /t { 60 | content_by_lua_block { 61 | local rc = require("resty.redis.connector").new() 62 | local rs = require("resty.redis.sentinel") 63 | 64 | local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1", db = ngx.null } 65 | assert(sentinel and not err, "sentinel should connect without errors but got " .. tostring(err)) 66 | 67 | local master, err = rs.get_master(sentinel, "mymaster") 68 | 69 | assert(master and not err, "get_master should return the master") 70 | 71 | assert(master.host == "127.0.0.1" and tonumber(master.port) == $TEST_NGINX_REDIS_PORT, 72 | "host should be 127.0.0.1 and port should be $TEST_NGINX_REDIS_PORT") 73 | 74 | master, err = rs.get_master(sentinel, "invalid-mymaster") 75 | 76 | assert(not master and err, "invalid master name should result in error") 77 | 78 | sentinel:close() 79 | } 80 | } 81 | --- request 82 | GET /t 83 | --- no_error_log 84 | [error] 85 | 86 | 87 | === TEST 2b: Get the master directly 88 | --- http_config eval: $::HttpConfig 89 | --- config 90 | location /t { 91 | content_by_lua_block { 92 | local rc = require("resty.redis.connector").new() 93 | 94 | local master, err = rc:connect({ 95 | url = "sentinel://mymaster:m/3", 96 | sentinels = { 97 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 } 98 | } 99 | }) 100 | 101 | assert(master and not err, "get_master should return the master") 102 | assert(master:set("foo", "bar"), "set should run without error") 103 | assert(master:get("foo") == "bar", "get(foo) should return bar") 104 | 105 | master:close() 106 | } 107 | } 108 | --- request 109 | GET /t 110 | --- no_error_log 111 | [error] 112 | 113 | 114 | === TEST 3: Get slaves 115 | --- http_config eval: $::HttpConfig 116 | --- config 117 | location /t { 118 | content_by_lua_block { 119 | local rc = require("resty.redis.connector").new() 120 | local rs = require("resty.redis.sentinel") 121 | 122 | local sentinel, err = rc:connect{ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1", db = ngx.null } 123 | assert(sentinel and not err, "sentinel should connect without error") 124 | 125 | local slaves, err = rs.get_slaves(sentinel, "mymaster") 126 | 127 | assert(slaves and not err, "slaves should be returned without error") 128 | 129 | local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false } 130 | 131 | for _,slave in ipairs(slaves) do 132 | slaveports[tostring(slave.port)] = true 133 | end 134 | 135 | assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true, 136 | "slaves should both be found") 137 | 138 | slaves, err = rs.get_slaves(sentinel, "invalid-mymaster") 139 | 140 | assert(not slaves and err, "invalid master name should result in error") 141 | 142 | sentinel:close() 143 | } 144 | } 145 | --- request 146 | GET /t 147 | --- no_error_log 148 | [error] 149 | 150 | 151 | === TEST 4: Get only healthy slaves 152 | --- http_config eval: $::HttpConfig 153 | --- config 154 | location /t { 155 | content_by_lua_block { 156 | local rc = require("resty.redis.connector").new() 157 | 158 | local sentinel, err = rc:connect({ url = "redis://127.0.0.1:$TEST_NGINX_SENTINEL_PORT1", db = ngx.null }) 159 | assert(sentinel and not err, "sentinel should connect without error") 160 | 161 | local slaves, err = require("resty.redis.sentinel").get_slaves( 162 | sentinel, 163 | "mymaster" 164 | ) 165 | 166 | assert(slaves and not err, "slaves should be returned without error") 167 | 168 | local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false } 169 | 170 | for _,slave in ipairs(slaves) do 171 | slaveports[tostring(slave.port)] = true 172 | end 173 | 174 | assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == true and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true, 175 | "slaves should both be found") 176 | 177 | -- connect to one and remove it 178 | local r = require("resty.redis.connector").new():connect({ 179 | port = $TEST_NGINX_REDIS_PORT_SL1, 180 | }) 181 | r:slaveof("127.0.0.1", 7000) 182 | 183 | ngx.sleep(9) 184 | 185 | local slaves, err = require("resty.redis.sentinel").get_slaves( 186 | sentinel, 187 | "mymaster" 188 | ) 189 | 190 | assert(slaves and not err, "slaves should be returned without error") 191 | 192 | local slaveports = { ["$TEST_NGINX_REDIS_PORT_SL1"] = false, ["$TEST_NGINX_REDIS_PORT_SL2"] = false } 193 | 194 | for _,slave in ipairs(slaves) do 195 | slaveports[tostring(slave.port)] = true 196 | end 197 | 198 | assert(slaveports["$TEST_NGINX_REDIS_PORT_SL1"] == false and slaveports["$TEST_NGINX_REDIS_PORT_SL2"] == true, 199 | "only $TEST_NGINX_REDIS_PORT_SL2 should be found") 200 | 201 | r:slaveof("127.0.0.1", $TEST_NGINX_REDIS_PORT) 202 | 203 | sentinel:close() 204 | } 205 | } 206 | --- request 207 | GET /t 208 | --- timeout: 10 209 | --- no_error_log 210 | [error] 211 | 212 | 213 | === TEST 5: connector.connect_via_sentinel 214 | --- http_config eval: $::HttpConfig 215 | --- config 216 | location /t { 217 | content_by_lua_block { 218 | local rc = require("resty.redis.connector").new() 219 | 220 | local params = { 221 | sentinels = { 222 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 }, 223 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 }, 224 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 }, 225 | }, 226 | master_name = "mymaster", 227 | role = "master", 228 | } 229 | 230 | local redis, err = rc:connect_via_sentinel(params) 231 | assert(redis and not err, "redis should connect without error") 232 | 233 | params.role = "slave" 234 | 235 | local redis, err = rc:connect_via_sentinel(params) 236 | assert(redis and not err, "redis should connect without error") 237 | } 238 | } 239 | --- request 240 | GET /t 241 | --- no_error_log 242 | [error] 243 | 244 | 245 | === TEST 6: regression for slave sorting (iss12) 246 | --- http_config eval: $::HttpConfig 247 | --- config 248 | location /t { 249 | lua_socket_log_errors Off; 250 | content_by_lua_block { 251 | local rc = require("resty.redis.connector").new() 252 | 253 | local params = { 254 | sentinels = { 255 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT1 }, 256 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT2 }, 257 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT3 }, 258 | }, 259 | master_name = "mymaster", 260 | role = "slave", 261 | } 262 | 263 | -- hotwire get_slaves to expose sorting issue 264 | local sentinel = require("resty.redis.sentinel") 265 | sentinel.get_slaves = function() 266 | return { 267 | { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT_SL1 }, 268 | { host = "127.0.0.1", port = $TEST_NGINX_REDIS_PORT_SL2 }, 269 | { host = "134.123.51.2", port = $TEST_NGINX_REDIS_PORT_SL1 }, 270 | } 271 | end 272 | 273 | local redis, err = rc:connect_via_sentinel(params) 274 | assert(redis and not err, "redis should connect without error") 275 | } 276 | } 277 | --- request 278 | GET /t 279 | --- no_error_log 280 | [error] 281 | 282 | === TEST 7: connect with acl 283 | --- http_config eval: $::HttpConfig 284 | --- config 285 | location /t { 286 | content_by_lua_block { 287 | local rc = require("resty.redis.connector").new() 288 | local redis, err = rc:connect({ 289 | username = "redisuser", 290 | password = "redisuserpass", 291 | sentinels = { 292 | { host = "127.0.0.1", port = $TEST_NGINX_SENTINEL_PORT_AUTH } 293 | }, 294 | master_name = "mymaster", 295 | sentinel_username = "sentineluser", 296 | sentinel_username = "sentineluserpass", 297 | }) 298 | assert(redis and not err, "redis should connect without error") 299 | local username = assert(redis:acl("whoami")) 300 | assert(username == "redisuser", "should connect as 'redisuser' but got " .. tostring(username)) 301 | } 302 | } 303 | --- request 304 | GET /t 305 | --- no_error_log 306 | [error] 307 | -------------------------------------------------------------------------------- /util/lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | sub file_contains ($$); 7 | 8 | my $version; 9 | for my $file (map glob, qw{ *.lua lib/*.lua lib/*/*.lua lib/*/*/*.lua lib/*/*/*/*.lua lib/*/*/*/*/*.lua }) { 10 | # Check the sanity of each .lua file 11 | open my $in, $file or 12 | die "ERROR: Can't open $file for reading: $!\n"; 13 | my $found_ver; 14 | while (<$in>) { 15 | my ($ver, $skipping); 16 | if (/(?x) (?:_VERSION) \s* = .*? ([\d\.]*\d+) (.*? SKIP)?/) { 17 | my $orig_ver = $ver = $1; 18 | $found_ver = 1; 19 | # $skipping = $2; 20 | $ver =~ s{^(\d+)\.(\d{3})(\d{3})$}{join '.', int($1), int($2), int($3)}e; 21 | warn "$file: $orig_ver ($ver)\n"; 22 | 23 | } elsif (/(?x) (?:_VERSION) \s* = \s* ([a-zA-Z_]\S*)/) { 24 | warn "$file: $1\n"; 25 | $found_ver = 1; 26 | last; 27 | } 28 | 29 | if ($ver and $version and !$skipping) { 30 | if ($version ne $ver) { 31 | # die "$file: $ver != $version\n"; 32 | } 33 | } elsif ($ver and !$version) { 34 | $version = $ver; 35 | } 36 | } 37 | if (!$found_ver) { 38 | warn "WARNING: No \"_VERSION\" or \"version\" field found in `$file`.\n"; 39 | } 40 | close $in; 41 | 42 | #print "Checking use of Lua global variables in file $file ...\n"; 43 | 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)\$'"); 44 | #file_contains($file, "attempt to write to undeclared variable"); 45 | system("grep -H -n -E --color '.{120}' $file"); 46 | } 47 | 48 | sub file_contains ($$) { 49 | my ($file, $regex) = @_; 50 | open my $in, $file 51 | or die "Cannot open $file fo reading: $!\n"; 52 | my $content = do { local $/; <$in> }; 53 | close $in; 54 | #print "$content"; 55 | return scalar ($content =~ /$regex/); 56 | } 57 | 58 | if (-d 't') { 59 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 60 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 61 | } 62 | } 63 | 64 | --------------------------------------------------------------------------------