├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md ├── dist.ini ├── example ├── README.md ├── load-balancer.lua └── nginx.conf ├── lib └── resty │ └── upstream │ ├── api.lua │ ├── http.lua │ └── socket.lua ├── lua-resty-upstream-0.08-0.rockspec ├── t ├── 01-socket.t ├── 02-pools.t ├── 03-hosts.t ├── 04-api-pools.t ├── 05-api-hosts.t ├── 06-http.t ├── 07-locking.t ├── 08-round_robin.t ├── 09-events.t ├── 10-http-healthcheck.t └── 11_hash.t └── util └── lua-releng.pl /.gitignore: -------------------------------------------------------------------------------- 1 | t/servroot/ 2 | t/error.log 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Hamish Forbes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | TEST_FILE ?= t 8 | 9 | .PHONY: all test leak 10 | 11 | all: ; 12 | 13 | 14 | install: all 15 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/upstream 16 | $(INSTALL) lib/resty/upstream/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty/upstream 17 | 18 | leak: all 19 | TEST_NGINX_CHECK_LEAK=1 TEST_NGINX_NO_SHUFFLE=1 PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(TEST_FILE) 20 | 21 | test: all 22 | TEST_NGINX_NO_SHUFFLE=1 PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(TEST_FILE) 23 | util/lua-releng.pl 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-upstream 2 | 3 | Upstream connection load balancing and failover module 4 | 5 | # Table of Contents 6 | 7 | * [Status](#status) 8 | * [Overview](#overview) 9 | * [upstream.socket](#upstream.socket) 10 | * [new](#new) 11 | * [init_background_thread](#init_background_thread) 12 | * [connect](#connect) 13 | * [process_failed_hosts](#process_failed_hosts) 14 | * [get_pools](#get_pools) 15 | * [save_pools](#save_pools) 16 | * [sort_pools](#sort_pools) 17 | * [bind](#bind) 18 | * [upstream.api](#upstream.api) 19 | * [new](#new-1) 20 | * [set_method](#set_method) 21 | * [create_pool](#create_pool) 22 | * [set_priority](#set_priority) 23 | * [add_host](#add_host) 24 | * [remove_host](#remove_host) 25 | * [down_host](#down_host) 26 | * [up_host](#up_host) 27 | * [upstream.http](#upstream.http) 28 | * [status_codes](#status_codes) 29 | * [new](#new-2) 30 | * [init_background_thread](#init_background_thread-1) 31 | * [request](#request) 32 | * [set_keepalive](#set_keepalive) 33 | * [get_reused_times](#get_reused_times) 34 | * [close](#close) 35 | * [HTTP Healthchecks](#http-healthchecks) 36 | 37 | 38 | # Status 39 | 40 | Experimental, API may change without warning. 41 | 42 | Requires ngx_lua > 0.9.5 43 | 44 | # Overview 45 | 46 | Create a lua [shared dictionary](https://github.com/openresty/lua-nginx-module#lua_shared_dict). 47 | Define your upstream pools and hosts in init_by_lua, this will be saved into the shared dictionary. 48 | 49 | Use the `connect` method to return a connected tcp [socket](https://github.com/openresty/lua-nginx-module#ngxsockettcp). 50 | 51 | Alternatively pass in a resty module (e.g [lua-resty-redis](https://github.com/openresty/lua-resty-redis) or [lua-resty-http](https://github.com/pintsized/lua-resty-http)) that implements `connect()` and `set_timeout()`. 52 | 53 | Call `process_failed_hosts` to handle failed hosts without blocking current request. 54 | 55 | Use `resty.upstream.api` to modify upstream configuration during init or runtime, this is recommended! 56 | 57 | `resty.upstream.http` wraps the [lua-resty-http](https://github.com/pintsized/lua-resty-http) from @pintsized. 58 | 59 | It allows for failover based on HTTP status codes as well as socket connection status. 60 | 61 | 62 | ```lua 63 | lua_shared_dict my_upstream_dict 1m; 64 | init_by_lua ' 65 | upstream_socket = require("resty.upstream.socket") 66 | upstream_api = require("resty.upstream.api") 67 | 68 | upstream, configured = upstream_socket:new("my_upstream_dict") 69 | if not upstream then 70 | error(configured) 71 | end 72 | api = upstream_api:new(upstream) 73 | 74 | if not configured then -- Only reconfigure on start, shared mem persists across a HUP 75 | api:create_pool({id = "primary", timeout = 100}) 76 | api:set_priority("primary", 0) 77 | api:set_method("primary", "round_robin") 78 | api:add_host("primary", { id="a", host = "127.0.0.1", port = "80", weight = 10 }) 79 | api:add_host("primary", { id="b", host = "127.0.0.1", port = "81", weight = 10 }) 80 | 81 | api:create_pool({id = "dr"}) 82 | api:set_priority("dr", 10) 83 | api:add_host("dr", { host = "127.0.0.1", port = "82", weight = 5 }) 84 | api:add_host("dr", { host = "127.0.0.1", port = "83", weight = 10 }) 85 | 86 | api:create_pool({id = "test", priority = 5}) 87 | api:add_host("primary", { id="c", host = "127.0.0.1", port = "82", weight = 10 }) 88 | api:add_host("primary", { id="d", host = "127.0.0.1", port = "83", weight = 10 }) 89 | end 90 | '; 91 | 92 | init_worker_by_lua 'upstream:init_background_thread()'; 93 | 94 | server { 95 | 96 | location / { 97 | content_by_lua ' 98 | local sock, err = upstream:connect() 99 | upstream:process_failed_hosts() 100 | '; 101 | } 102 | 103 | } 104 | ``` 105 | 106 | # upstream.socket 107 | 108 | ### new 109 | `syntax: upstream, configured = upstream_socket:new(dictionary, id?)` 110 | 111 | Returns a new upstream object using the provided dictionary name. 112 | When called in init_by_lua returns an additional variable if the dictionary already contains configuration. 113 | Takes an optional id parameter, this *must* be unique if multiple instances of upstream.socket are using the same dictionary. 114 | 115 | ### init_background_thread 116 | `syntax: ok, err = upstream:init_background_thread()` 117 | 118 | Initialises the background thread, should be called in `init_worker_by_lua` 119 | 120 | ### connect 121 | `syntax: ok, err = upstream:connect(client?, key?)` 122 | 123 | Attempts to connect to a host in the defined pools in priority order using the selected load balancing method. 124 | Returns a connected socket and a table containing the connected `host`, `poolid` and `pool` or nil and an error message. 125 | 126 | When passed a [socket](https://github.com/openresty/lua-nginx-module#ngxsockettcp) or resty module it will return the same object after successful connection or nil. 127 | 128 | Additionally, hash methods may take an optional `key` to define how to hash the connection to determine the host. By default `ngx.var.remote_addr` is used. This value is ignored when the pool's method is round robin. 129 | 130 | ```lua 131 | resty_redis = require('resty.redis') 132 | local redis = resty_redis.new() 133 | 134 | local key = ngx.req.get_headers()["X-Forwarded-For"] 135 | 136 | local redis, err = upstream:connect(redis, key) 137 | 138 | if not redis then 139 | ngx.log(ngx.ERR, err) 140 | ngx.status = 500 141 | return ngx.exit(ngx.status) 142 | end 143 | 144 | ngx.log(ngx.info, 'Connected to ' .. err.host.host .. ':' .. err.host.port) 145 | local ok, err = redis:get('key') 146 | ``` 147 | 148 | ### process_failed_hosts 149 | `syntax: ok, err = upstream:process_failed_hosts()` 150 | 151 | Processes any failed or recovered hosts from the current request. 152 | Spawns an immediate callback via [ngx.timer.at](https://github.com/openresty/lua-nginx-module#ngxtimerat), does not block current request. 153 | 154 | 155 | ### get_pools 156 | `syntax: pools = usptream:get_pools()` 157 | 158 | Returns a table containing the current pool and host configuration. 159 | e.g. 160 | 161 | ```lua 162 | { 163 | primary = { 164 | up = true, 165 | method = 'round_robin', 166 | timeout = 100, 167 | priority = 0, 168 | hosts = { 169 | web01 = { 170 | host = "127.0.0.1", 171 | weight = 10, 172 | port = "80", 173 | lastfail = 0, 174 | failcount = 0, 175 | up = true, 176 | healthcheck = true 177 | } 178 | web02 = { 179 | host = "127.0.0.1", 180 | weight = 10, 181 | port = "80", 182 | lastfail = 0, 183 | failcount = 0, 184 | up = true, 185 | healthcheck = { interval = 30, path = '/check' } 186 | } 187 | } 188 | }, 189 | secondary = { 190 | up = true, 191 | method = 'round_robin', 192 | timeout = 2000, 193 | priority = 10, 194 | hosts = { 195 | dr01 = { 196 | host = "10.10.10.1", 197 | weight = 10, 198 | port = "80", 199 | lastfail = 0, 200 | failcount = 0, 201 | up = true 202 | } 203 | 204 | } 205 | }, 206 | } 207 | ``` 208 | 209 | ### save_pools 210 | `syntax: ok, err = upstream:save_pools(pools)` 211 | 212 | Saves a table of pools to the shared dictionary, `pools` must be in the same format as returned from `get_pools` 213 | 214 | ### sort_pools 215 | `syntax: ok, err = upstream:sort_pools(pools)` 216 | 217 | Generates a priority order in the shared dictionary based on the table of pools provided 218 | 219 | ### bind 220 | `syntax: ok, err = upstream:bind(event, func)` 221 | 222 | Bind a function to be called when events occur. `func` should expect 1 argument containing event data. 223 | 224 | Returns `true` on a successful bind or `nil` and an error message on failure. 225 | 226 | ```lua 227 | local function host_down_handler(event) 228 | ngx.log(ngx.ERR, "Host: ", event.host.host, ":", event.host.port, " in pool '", event.pool.id,'" is down!') 229 | end 230 | local ok, err = upstream:bind('host_down', host_down_handler) 231 | ``` 232 | 233 | #### Event: host_up 234 | 235 | Fired when a host changes status from down to up. 236 | Event data is a table containing the affected host and pool. 237 | 238 | #### Event: host_down 239 | 240 | Fired when a host changes status from up to down. 241 | Event data is a table containing the affected host and pool. 242 | 243 | 244 | # upstream.api 245 | These functions allow you to dynamically reconfigure upstream pools and hosts 246 | 247 | ### new 248 | `syntax: api, err = upstream_api:new(upstream)` 249 | 250 | Returns a new api object using the provided upstream object. 251 | 252 | 253 | ### set_method 254 | `syntax: ok, err = api:set_method(poolid, method)` 255 | 256 | Sets the load balancing method for the specified pool. 257 | Currently randomised round robin and hashing methods are supported. 258 | 259 | ### create_pool 260 | `syntax: ok, err = api:create_pool(pool)` 261 | 262 | Creates a new pool from a table of options, `pool` must contain at least 1 key `id` which must be unique within the current upstream object. 263 | 264 | Other valid options are 265 | 266 | * `method` Balancing method 267 | * `timeout` Connection timeout in ms 268 | * `priority` Higher priority pools are used later 269 | * `read_timeout` 270 | * `keepalive_timeout` 271 | * `keepalive_pool` 272 | * `status_codes` See [status_codes](#status_codes) 273 | 274 | Hosts cannot be defined at this point. 275 | 276 | Note: IDs are converted to a string by this function 277 | 278 | Default pool values 279 | ```lua 280 | { method = 'round_robin', timeout = 2000, priority = 0 } 281 | ``` 282 | 283 | ### set_priority 284 | `syntax: ok, err = api:set_priority(poolid, priority)` 285 | 286 | Priority must be a number, returns nil on error. 287 | 288 | ### add_host 289 | `syntax: ok, err = api:add_host(poolid, host)` 290 | 291 | Takes a pool ID and a table of options, `host` must contain at least `host`. 292 | If the host ID is not specified it will be a numeric index based on the number of hosts in the pool. 293 | 294 | Note: IDs are converted to a string by this function 295 | 296 | Defaults: 297 | ```lua 298 | { host = '', port = 80, weight = 0} 299 | ``` 300 | 301 | ### remove_host 302 | `syntax: ok, err = api:remove_host(poolid, host)` 303 | 304 | Takes a poolid and a hostid to remove from the pool 305 | 306 | ### down_host 307 | `syntax: ok,err = api:down_host(poolid, host)` 308 | 309 | Manually marks a host as down, this host will *not* be revived automatically. 310 | 311 | ### up_host 312 | `syntax: ok,err = api:up_host(poolid, host)` 313 | 314 | Manually restores a dead host to the pool 315 | 316 | # upstream.http 317 | 318 | Functions for making http requests to upstream hosts. 319 | 320 | ### status_codes 321 | This pool option is an array of status codes that indicate a failed request. Defaults to none. 322 | 323 | The `x` character masks a digit 324 | 325 | ```lua 326 | { 327 | ['5xx'] = true, -- Matches 500, 503, 524 328 | ['400'] = true -- Matches only 400 329 | } 330 | ``` 331 | 332 | 333 | ### new 334 | `syntax: httpc, err = upstream_http:new(upstream, ssl_opts?)` 335 | 336 | Returns a new http upstream object using the provided upstream object. 337 | 338 | `ssl_opts` is an optional table for configuring SSL support. 339 | * `ssl` set to `true` to enable SSL Handshaking, default `false` 340 | * `ssl_verify` set to `false` to disable SSL certificate verification, default `true` 341 | * `sni_host` a string to use as the sni hostname, default is the request's Host header 342 | 343 | ```lua 344 | https_upstream = Upstream_HTTP:new(upstream_ssl, { 345 | ssl = true, 346 | ssl_verify = true, 347 | sni_host = "foo.example.com" 348 | }) 349 | ``` 350 | 351 | ### init_background_thread 352 | `syntax: ok, err = upstream_http:init_background_thread()` 353 | 354 | Initialises the background thread, should be called in `init_worker_by_lua`. 355 | 356 | Do *not* call the `init_background_thread` method in `upstream.socket` if using the `upstream.http` background thread 357 | 358 | ### request 359 | `syntax: res, err_or_conn_info, status? = upstream_api:request(params)` 360 | 361 | Takes the same parameters as lua-resty-http's [request](https://github.com/pintsized/lua-resty-http#request) method. 362 | 363 | On a successful request returns the lua-resty-http object and a table containing the connected host and pool. 364 | 365 | If the request failed returns nil, the error and a suggested http status code 366 | 367 | ```lua 368 | local ok, err, status = upstream_http:request({ 369 | path = "/helloworld", 370 | headers = { 371 | ["Host"] = "example.com", 372 | } 373 | }) 374 | if not ok then 375 | ngx.status = status 376 | ngx.say(err) 377 | ngx.exit(status) 378 | else 379 | local host = err.host 380 | local pool = err.pool 381 | end 382 | ``` 383 | 384 | ### set_keepalive 385 | `syntax: ok, err = upstream_http:set_keepalive()` 386 | 387 | Passes the keepalive timeout / pool from the pool configuration through to the lua-resty-http `set_keepalive` method. 388 | 389 | ### get_reused_times 390 | `syntax: ok, err = upstream_http:get_reused_times()` 391 | 392 | Passes through to the lua-resty-http `get_reused_times` method. 393 | 394 | ### close 395 | `syntax: ok, err = upstream_http:close()` 396 | 397 | Passes through to the lua-resty-http `close` method. 398 | 399 | 400 | 401 | ## HTTP Healthchecks 402 | 403 | Active background healthchecks can be enabled by adding the `healthcheck` parameter to a host. 404 | 405 | A value of `true` will enable the default check, a `GET` request for `/`. 406 | 407 | The `healthcheck` parameter can also be a table of parameters valid for lua-resty-http's [request](https://github.com/pintsized/lua-resty-http#request) method. 408 | 409 | With a few additional parameters 410 | 411 | * `interval` to set the time between healthchecks, in seconds. Must be >= 10s. Defaults to 60s 412 | * `timeout` sets the connect timeout for healthchecks. Defaults to pool setting. 413 | * `read_timeout` sets the read timeout for healthchecks. Defaults to pool setting. 414 | * `status_codes` a table of invalid response status codes. Defaults to pool setting. 415 | 416 | Failure for the background check is according to the same parameters as for a frontend request, unless overriden explicitly. 417 | 418 | ```lua 419 | -- Custom check parameters 420 | api:add_host("primary", { 421 | host = 123.123.123.123, 422 | port = 80, 423 | healthcheck = { 424 | interval = 30, -- check every 30s 425 | timeout = (5*1000), -- 5s connect timeout 426 | read_timeout = (15*1000), -- 15s connect timeout 427 | status_codes = {["5xx"] = true, ["403"] = true}, -- 5xx and 403 responses are a fail 428 | -- resty-http params 429 | path = "/check", 430 | headers = { 431 | ["Host"] = "domain.com", 432 | ["Accept-Encoding"] = "gzip" 433 | } 434 | } 435 | }) 436 | 437 | -- Default check parameters 438 | api:add_host("primary", {host = 123.123.123.123, port = 80, healthcheck = true}) 439 | 440 | ``` 441 | 442 | 443 | ## TODO 444 | * IP based sticky sessions 445 | * Slow start - recovered hosts have lower weighting 446 | * Active TCP healthchecks 447 | * Use Cap'n Proto instead of JSON for serialisation 448 | * HTTP Minimum Rises - Hosts must have n succesful healthchecks before being marked up 449 | * HTTP Specific options 450 | * Cookie based sticky sessions 451 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-upstream 2 | abstract=Upstream connection load balancing and failover module 3 | author=Hamish Forbes 4 | is_original=yes 5 | license=mit 6 | lib_dir=lib 7 | doc_dir=example/ 8 | repo_link=https://github.com/hamishforbes/lua-resty-upstream 9 | main_module=lib/resty/upstream/socket.lua 10 | requires=ledgetech/lua-resty-http 11 | exclude_files=lib/resty/upstream/healthcheck.lua # Conflict with lua-resty-upstream-healthcheck 12 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Load Balancer example 2 | 3 | This openresty config and lua script show an example of a simple HTTP load balancer using lua-resty-upstream and lua-resty-http. 4 | 5 | ## init_by_lua 6 | 7 | In `init_by_lua` we've pulled in all 3 upstream modules, socket, http and api. 8 | 9 | Then we create a new socket upstream instance and define 2 pools, `primary` and `dr`, each with 2 hosts. 10 | Set the IPs and ports to whatever is appropriate for your environment. 11 | The pools have some keepalive and timeout settings configured 12 | 13 | Instances are created for both the API and http upstream modules. 14 | 15 | Repeat for the SSL enable origin servers. 16 | 17 | ## init_worker_by_lua 18 | 19 | We call `init_background_thread()` here on both http upstream instances to start the background workers. 20 | 21 | This worker will restore dead hosts after the defined timeout period and perform background checks on hosts. 22 | 23 | 24 | ## lua-load-balancer 25 | 26 | In our main server block, listening on port 80 and on port 443 for ssl, we pass everything to `load-balancer.lua` in `content_by_lua`. 27 | 28 | In `load-balancer.lua` we check the scheme variable and select the right upstream instance. 29 | `httpc` can then be used as if it was an instance of lua-resty-http. 30 | The only real difference is the second return value from `request()` is a table. 31 | 32 | We first get the request body iterator and return a 411 error if the client is attempting to send a chunked request. 33 | This is not yet supported by the ngx_lua `ngx.req.socket` api. 34 | 35 | Then we make an http request with all the parameters of the current request. 36 | If the request errors then the `conn_info` table will contain the error message in `err` and the recommended response status code in `status`. 37 | This will be `504 Gateway Timeout` if no tcp connection could be made at all, or `502 Bad Gateway` if the upstream host returned a bad status code. 38 | 39 | You don't *have* to use these status codes, you can do whatever you like at this point. 40 | 41 | If we successfully made a request to one of the hosts then we strip out hop-by-hop headers and add the rest of the upstream response headers to the current request's response headers. 42 | Then we read the response body, if available, from the upstream host in chunks and flush back to the client. 43 | 44 | We call `set_keepalive()` to let the pool configuration and http response determine whether to close the socket or put it into the connection pool. 45 | 46 | Lastly we call `process_failed_hosts()` on the socket upstream module to save any failed hosts back to the dictionary. 47 | This function triggers an immediate callback to run once the request has finished and so this doesn't affect the response time for the client. 48 | 49 | ## api 50 | 51 | On port 8080 theres a very simple HTTP API, requesting `/pools` will return the current json encoded pool definition. 52 | Calling `/down_host/primary/1` will mark the first host in the primary pool as offline and immediately stop any further requests being made to it. 53 | Likewise `/up_host/primary/1` will bring it back up. 54 | -------------------------------------------------------------------------------- /example/load-balancer.lua: -------------------------------------------------------------------------------- 1 | local ngx_log = ngx.log 2 | local ngx_ERR = ngx.ERR 3 | local flush = ngx.flush 4 | local print = ngx.print 5 | local req = ngx.req 6 | local ngx_var = ngx.var 7 | local str_lower = string.lower 8 | local res_header = ngx.header 9 | 10 | local httpc = http_upstream 11 | local upstream = upstream 12 | 13 | if ngx.var.scheme == "https" then 14 | httpc = https_upstream 15 | upstream = upstream_ssl 16 | end 17 | 18 | local client_body_reader, err = httpc:get_client_body_reader() 19 | if not client_body_reader then 20 | if err == "chunked request bodies not supported yet" then 21 | ngx.status = 411 22 | ngx.say("411 Length Required") 23 | ngx.exit(ngx.status) 24 | return 25 | elseif err ~= nil then 26 | ngx_log(ngx_ERR, "Error getting client body reader: ", err) 27 | end 28 | end 29 | 30 | local res, conn_info = httpc:request{ 31 | method = req.get_method(), 32 | path = (ngx_var.uri .. ngx_var.is_args .. (ngx_var.args or "")), 33 | body = client_body_reader, 34 | headers = req.get_headers(), 35 | } 36 | 37 | if not res then 38 | ngx.status = conn_info.status 39 | ngx.say(conn_info.err) 40 | return ngx.exit(ngx.status) 41 | end 42 | 43 | ngx.status = res.status 44 | 45 | local HOP_BY_HOP_HEADERS = { 46 | ["connection"] = true, 47 | ["keep-alive"] = true, 48 | ["proxy-authenticate"] = true, 49 | ["proxy-authorization"] = true, 50 | ["te"] = true, 51 | ["trailers"] = true, 52 | ["transfer-encoding"] = true, 53 | ["upgrade"] = true, 54 | } 55 | 56 | for k,v in pairs(res.headers) do 57 | if not HOP_BY_HOP_HEADERS[str_lower(k)] then 58 | res_header[k] = v 59 | end 60 | end 61 | 62 | local reader = res.body_reader 63 | if reader then 64 | repeat 65 | local chunk, err = reader(65536) 66 | if err then 67 | ngx_log(ngx_ERR, "Read Error: "..(err or "")) 68 | break 69 | end 70 | 71 | if chunk then 72 | print(chunk) 73 | flush(true) 74 | end 75 | until not chunk 76 | end 77 | 78 | local ok,err = httpc:set_keepalive() 79 | 80 | upstream:process_failed_hosts() 81 | 82 | -------------------------------------------------------------------------------- /example/nginx.conf: -------------------------------------------------------------------------------- 1 | events { 2 | worker_connections 1024; 3 | } 4 | 5 | error_log logs/error.log error; 6 | 7 | http { 8 | include mime.types; 9 | default_type application/octet-stream; 10 | 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" [$request_time]'; 14 | 15 | access_log logs/access.log main; 16 | resolver 8.8.8.8; 17 | 18 | ssl_certificate /path/to/your/cert.crt; 19 | ssl_certificate_key /path/to/your/key.key; 20 | ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA:AES128-GCM-SHA256:ECDHE-RSA-RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!CAMELLIA; 21 | ssl_prefer_server_ciphers on; 22 | ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; 23 | ssl_session_cache shared:SSL:10m; 24 | ssl_session_timeout 5m; 25 | 26 | lua_shared_dict my_upstream 1m; 27 | lua_socket_log_errors off; 28 | 29 | lua_package_path "/path/to/lua-resty-http/lib/?.lua;;"; 30 | 31 | init_by_lua ' 32 | Upstream_Socket = require("resty.upstream.socket") 33 | Upstream_HTTP = require("resty.upstream.http") 34 | Upstream_Api = require("resty.upstream.api") 35 | 36 | local configured 37 | 38 | upstream, configured = Upstream_Socket:new("my_upstream") 39 | if not upstream then 40 | error(configured) 41 | end 42 | api = Upstream_Api:new(upstream) 43 | http_upstream = Upstream_HTTP:new(upstream) 44 | 45 | if not configured then -- Only reconfigure on start, shared mem persists across a HUP 46 | api:create_pool({id = "primary", timeout = 100, read_timeout = 10000, keepalive_pool = 256, keepalive_timeout = (120*1000)}) 47 | 48 | api:add_host("primary", { host = "127.0.0.1", port = "81", weight = 10, healthcheck = true}) 49 | api:add_host("primary", { host = "127.0.0.1", port = "82", weight = 10, healthcheck = true}) 50 | 51 | api:create_pool({id = "dr", timeout = 100, priority = 10, read_timeout = 60000}) 52 | api:add_host("dr", { host = "10.10.10.10", port = "81", weight = 10}) 53 | api:add_host("dr", { host = "10.10.10.10", port = "82", weight = 10}) 54 | 55 | end 56 | 57 | upstream_ssl, configured = Upstream_Socket:new("my_upstream", "ssl_upstream") 58 | if not upstream_ssl then 59 | error(configured) 60 | end 61 | api_ssl = Upstream_Api:new(upstream_ssl) 62 | https_upstream = Upstream_HTTP:new(upstream_ssl, {ssl = true, ssl_verify = true, sni_host = "foo.example.com" }) 63 | 64 | if not configured then -- Only reconfigure on start, shared mem persists across a HUP 65 | api_ssl:create_pool({id = "primary", timeout = 100, read_timeout = 10000, keepalive_pool = 256, keepalive_timeout = (120*1000)}) 66 | 67 | api_ssl:add_host("primary", { host = "127.0.0.1", port = "83", weight = 10, healthcheck = true}) 68 | api_ssl:add_host("primary", { host = "127.0.0.1", port = "84", weight = 10, healthcheck = true}) 69 | 70 | api_ssl:create_pool({id = "dr", timeout = 100, priority = 10, read_timeout = 60000}) 71 | api_ssl:add_host("dr", { host = "10.10.10.10", port = "83", weight = 10}) 72 | api_ssl:add_host("dr", { host = "10.10.10.10", port = "84", weight = 10}) 73 | 74 | end 75 | 76 | '; 77 | 78 | init_worker_by_lua ' 79 | http_upstream:init_background_thread() 80 | https_upstream:init_background_thread() 81 | '; 82 | 83 | server { 84 | listen 80; 85 | listen 443 ssl; 86 | server_name lua-load-balancer; 87 | 88 | location / { 89 | content_by_lua_file /path/to/lua-resty-upstream/example/load-balancer.lua; 90 | } 91 | 92 | } 93 | 94 | # HTTP origins 95 | server { 96 | listen 81; 97 | location / { 98 | echo 'foo'; 99 | } 100 | } 101 | server { 102 | listen 82; 103 | location / { 104 | echo 'bar'; 105 | } 106 | } 107 | 108 | # HTTPS origins 109 | server { 110 | listen 83 ssl; 111 | location / { 112 | echo 'foo-ssl'; 113 | } 114 | } 115 | 116 | server { 117 | listen 84 ssl; 118 | location / { 119 | echo 'bar-ssl'; 120 | } 121 | } 122 | 123 | 124 | server { 125 | listen 8080; 126 | server_name api; 127 | 128 | location = /pools { 129 | content_by_lua ' 130 | local cjson = require("cjson") 131 | local pools, err = api:get_pools() 132 | if not pools then 133 | ngx.print(cjson.encode(err)) 134 | ngx.status = 500 135 | else 136 | ngx.print(cjson.encode(pools)) 137 | end 138 | '; 139 | } 140 | 141 | location ~ ^/down_host/([^[/]+)/([^[/]+)$ { 142 | content_by_lua ' 143 | local cjson = require("cjson") 144 | local host = ngx.var[2] 145 | local pool = ngx.var[1] 146 | local ok, err = api:down_host(pool, host) 147 | if not ok then 148 | ngx.print(cjson.encode(err)) 149 | ngx.status = 500 150 | end 151 | '; 152 | } 153 | location ~ ^/up_host/([^[/]+)/([^[/]+)$ { 154 | content_by_lua ' 155 | local cjson = require("cjson") 156 | local host = ngx.var[2] 157 | local pool = ngx.var[1] 158 | local ok, err = api:up_host(pool, host) 159 | if not ok then 160 | ngx.print(cjson.encode(err)) 161 | ngx.status = 500 162 | end 163 | '; 164 | } 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /lib/resty/upstream/api.lua: -------------------------------------------------------------------------------- 1 | local ngx_log = ngx.log 2 | local ngx_DEBUG = ngx.DEBUG 3 | local ngx_ERR = ngx.ERR 4 | local ngx_INFO = ngx.INFO 5 | local str_format = string.format 6 | local tostring = tostring 7 | 8 | local _M = { 9 | _VERSION = "0.10", 10 | } 11 | 12 | local mt = { __index = _M } 13 | 14 | local default_pool = { 15 | up = true, 16 | method = 'round_robin', 17 | timeout = 2000, -- socket connect timeout 18 | priority = 0, 19 | failed_timeout = 60, 20 | max_fails = 3, 21 | hosts = {} 22 | } 23 | 24 | local optional_pool = { 25 | ['read_timeout'] = true, -- socket timeout after connect 26 | ['keepalive_timeout'] = true, 27 | ['keepalive_pool'] = true, 28 | ['status_codes'] = true 29 | } 30 | 31 | local numerics = { 32 | 'priority', 33 | 'timeout', 34 | 'failed_timeout', 35 | 'max_fails', 36 | 'read_timeout', 37 | 'keepalive_timeout', 38 | 'keepalive_pool', 39 | 'port', 40 | 'weight', 41 | 'failcount', 42 | 'lastfail' 43 | } 44 | 45 | local default_host = { 46 | host = '', 47 | port = 80, 48 | up = true, 49 | weight = 1, 50 | failcount = 0, 51 | lastfail = 0 52 | } 53 | 54 | local http_healthcheck_required = { 55 | interval = 60, -- Run every time background function runs if nil 56 | last_check = 0, 57 | } 58 | 59 | local optional_host = { 60 | ['healthcheck'] = false 61 | } 62 | 63 | function _M.new(_, upstream) 64 | 65 | local self = { 66 | upstream = upstream 67 | } 68 | return setmetatable(self, mt) 69 | end 70 | 71 | 72 | function _M.get_pools(self, ...) 73 | return self.upstream:get_pools(...) 74 | end 75 | 76 | 77 | function _M.get_locked_pools(self, ...) 78 | return self.upstream:get_locked_pools(...) 79 | end 80 | 81 | 82 | function _M.unlock_pools(self, ...) 83 | return self.upstream:unlock_pools(...) 84 | end 85 | 86 | 87 | function _M.save_pools(self, ...) 88 | return self.upstream:save_pools(...) 89 | end 90 | 91 | 92 | function _M.sort_pools(self, ...) 93 | return self.upstream:sort_pools(...) 94 | end 95 | 96 | 97 | function _M.set_method(self, poolid, method) 98 | local available_methods = self.upstream.available_methods 99 | 100 | if not available_methods[method] then 101 | return nil, 'Method not found' 102 | end 103 | 104 | if not poolid then 105 | return nil, 'No pool ID specified' 106 | end 107 | poolid = tostring(poolid) 108 | 109 | local pools, err = self:get_locked_pools() 110 | if not pools then 111 | return nil, err 112 | end 113 | if not pools[poolid] then 114 | self:unlock_pools() 115 | return nil, 'Pool not found' 116 | end 117 | pools[poolid].method = method 118 | ngx_log(ngx_DEBUG, str_format('%s method set to %s', poolid, method)) 119 | 120 | local ok, err = self:save_pools(pools) 121 | if not ok then 122 | ngx_log(ngx_ERR, "Error saving pools for upstream ", self.id, " ", err) 123 | end 124 | 125 | self:unlock_pools() 126 | 127 | return ok, err 128 | end 129 | 130 | 131 | local function validate_pool(opts, pools, methods) 132 | if pools[tostring(opts.id)] then 133 | return nil, 'Pool exists' 134 | end 135 | 136 | for _,key in ipairs(numerics) do 137 | if opts[key] and type(opts[key]) ~= "number" then 138 | local tmp = tonumber(opts[key]) 139 | if not tmp then 140 | return nil, key.. " must be a number" 141 | else 142 | opts[key] = tmp 143 | end 144 | end 145 | end 146 | if opts.method and not methods[opts.method] then 147 | return nil, 'Method not available' 148 | end 149 | return true 150 | end 151 | 152 | 153 | function _M.create_pool(self, opts) 154 | local poolid = opts.id 155 | if not poolid then 156 | return nil, 'Pools must have an ID' 157 | end 158 | poolid = tostring(poolid) 159 | 160 | local pools, err = self:get_locked_pools() 161 | if not pools then 162 | return nil, err 163 | end 164 | 165 | local ok, err = validate_pool(opts, pools, self.upstream.available_methods) 166 | if not ok then 167 | self:unlock_pools() 168 | return ok, err 169 | end 170 | 171 | local pool = {} 172 | for k,v in pairs(default_pool) do 173 | local val = opts[k] or v 174 | -- Can't set 'up' or 'hosts' values here 175 | if k == 'up' or k == 'hosts' then 176 | val = v 177 | end 178 | pool[k] = val 179 | end 180 | -- Allow additional optional values 181 | for k,v in pairs(optional_pool) do 182 | if opts[k] then 183 | pool[k] = opts[k] 184 | end 185 | end 186 | pools[poolid] = pool 187 | 188 | local ok, err = self:save_pools(pools) 189 | if not ok then 190 | self:unlock_pools() 191 | return ok, err 192 | end 193 | 194 | -- Add some operational data per pool 195 | self.upstream.operational_data[poolid] = {} 196 | 197 | ngx_log(ngx_DEBUG, 'Created pool '..poolid) 198 | 199 | local ok, err = self:sort_pools(pools) 200 | self:unlock_pools() 201 | return ok, err 202 | end 203 | 204 | 205 | function _M.set_priority(self, poolid, priority) 206 | if type(priority) ~= 'number' then 207 | return nil, 'Priority must be a number' 208 | end 209 | if not poolid then 210 | return nil, 'No pool ID specified' 211 | end 212 | poolid = tostring(poolid) 213 | 214 | local pools, err = self:get_locked_pools() 215 | if not pools then 216 | return nil, err 217 | end 218 | if pools[poolid] == nil then 219 | self:unlock_pools() 220 | return nil, 'Pool not found' 221 | end 222 | 223 | pools[poolid].priority = priority 224 | 225 | local ok, err = self:save_pools(pools) 226 | if not ok then 227 | self:unlock_pools() 228 | return ok, err 229 | end 230 | ngx_log(ngx_DEBUG, str_format('%s priority set to %d', poolid, priority)) 231 | 232 | local ok, err = self:sort_pools(pools) 233 | self:unlock_pools() 234 | return ok, err 235 | end 236 | 237 | 238 | function _M.set_weight(self, poolid, hostid, weight) 239 | if type(weight) ~= 'number' or weight < 0 then 240 | return nil, 'Weight must be a positive number' 241 | end 242 | if not poolid or not hostid then 243 | return nil, 'Pool or host id not specified' 244 | end 245 | poolid = tostring(poolid) 246 | hostid = tostring(hostid) 247 | 248 | local pools, err = self:get_locked_pools() 249 | if not pools then 250 | return nil, err 251 | end 252 | 253 | local pool = pools[poolid] 254 | if pools[poolid] == nil then 255 | self:unlock_pools() 256 | return nil, 'Pool not found' 257 | end 258 | 259 | local host_idx = self.upstream.get_host_idx(hostid, pool.hosts) 260 | if not host_idx then 261 | self:unlock_pools() 262 | return nil, 'Host not found' 263 | end 264 | pool.hosts[host_idx].weight = weight 265 | 266 | ngx_log(ngx_DEBUG, 267 | str_format('Host weight "%s" in "%s" set to %d', hostid, poolid, weight) 268 | ) 269 | 270 | local ok,err = self:save_pools(pools) 271 | self:unlock_pools() 272 | return ok, err 273 | end 274 | 275 | 276 | function _M.add_host(self, poolid, host) 277 | if not host then 278 | return nil, 'No host specified' 279 | end 280 | if not poolid then 281 | return nil, 'No pool ID specified' 282 | end 283 | poolid = tostring(poolid) 284 | 285 | local pools, err = self:get_locked_pools() 286 | if not pools then 287 | return nil, err 288 | end 289 | if pools[poolid] == nil then 290 | self:unlock_pools() 291 | return nil, 'Pool not found' 292 | end 293 | local pool = pools[poolid] 294 | 295 | -- Validate host definition and set defaults 296 | local hostid = host['id'] 297 | if not hostid then 298 | hostid = #pool.hosts+1 299 | else 300 | hostid = tostring(hostid) 301 | for _, h in pairs(pool.hosts) do 302 | if h.id == hostid then 303 | self:unlock_pools() 304 | return nil, 'Host ID already exists' 305 | end 306 | end 307 | end 308 | hostid = tostring(hostid) 309 | 310 | local new_host = {} 311 | for key, default in pairs(default_host) do 312 | local val = host[key] 313 | if val == nil then val = default end 314 | new_host[key] = val 315 | end 316 | for key, default in pairs(optional_host) do 317 | if host[key] then 318 | new_host[key] = host[key] 319 | end 320 | end 321 | new_host.id = hostid 322 | 323 | for _,key in ipairs(numerics) do 324 | if new_host[key] and type(new_host[key]) ~= "number" then 325 | local tmp = tonumber(new_host[key]) 326 | if not tmp then 327 | self:unlock_pools() 328 | return nil, key.. " must be a number" 329 | else 330 | new_host[key] = tmp 331 | end 332 | end 333 | end 334 | 335 | -- Set http healthcheck minimum attributes 336 | local http_check = new_host.healthcheck 337 | if http_check then 338 | if http_check == true then 339 | new_host.healthcheck = http_healthcheck_required 340 | else 341 | for k,v in pairs(http_healthcheck_required) do 342 | if not http_check[k] then 343 | http_check[k] = v 344 | end 345 | end 346 | end 347 | end 348 | 349 | pool.hosts[#pool.hosts+1] = new_host 350 | 351 | ngx_log(ngx_DEBUG, str_format('Host "%s" added to "%s"', hostid, poolid)) 352 | local ok, err = self:save_pools(pools) 353 | self:unlock_pools() 354 | return ok,err 355 | end 356 | 357 | 358 | function _M.remove_host(self, poolid, hostid) 359 | if not poolid or not hostid then 360 | return nil, 'Pool or host id not specified' 361 | end 362 | poolid = tostring(poolid) 363 | hostid = tostring(hostid) 364 | 365 | local pools, err = self:get_locked_pools() 366 | if not pools then 367 | return nil, err 368 | end 369 | local pool = pools[poolid] 370 | if not pool then 371 | self:unlock_pools() 372 | return nil, 'Pool not found' 373 | end 374 | 375 | local host_idx = self.upstream.get_host_idx(hostid, pool.hosts) 376 | if not host_idx then 377 | self:unlock_pools() 378 | return nil, 'Host not found' 379 | end 380 | pool.hosts[host_idx] = nil 381 | 382 | ngx_log(ngx_DEBUG, str_format('Host "%s" removed from "%s"', hostid, poolid)) 383 | local ok, err = self:save_pools(pools) 384 | self:unlock_pools() 385 | return ok, err 386 | end 387 | 388 | 389 | function _M.down_host(self, poolid, hostid) 390 | if not poolid or not hostid then 391 | return nil, 'Pool or host id not specified' 392 | end 393 | poolid = tostring(poolid) 394 | hostid = tostring(hostid) 395 | 396 | local pools, err = self:get_locked_pools() 397 | if not pools then 398 | return nil, err 399 | end 400 | local pool = pools[poolid] 401 | if not pool then 402 | self:unlock_pools() 403 | return nil, 'Pool '.. poolid ..' not found' 404 | end 405 | local host_idx = self.upstream.get_host_idx(hostid, pool.hosts) 406 | if not host_idx then 407 | self:unlock_pools() 408 | return nil, 'Host not found' 409 | end 410 | local host = pool.hosts[host_idx] 411 | 412 | host.up = false 413 | host.lastfail = 0 414 | host.failcount = 0 415 | ngx_log(ngx_DEBUG, 416 | str_format('Host "%s" in Pool "%s" is manually down', 417 | host.id, 418 | poolid 419 | ) 420 | ) 421 | 422 | local ok, err = self:save_pools(pools) 423 | self:unlock_pools() 424 | return ok, err 425 | end 426 | 427 | 428 | function _M.up_host(self, poolid, hostid) 429 | if not poolid or not hostid then 430 | return nil, 'Pool or host id not specified' 431 | end 432 | poolid = tostring(poolid) 433 | hostid = tostring(hostid) 434 | 435 | local pools, err = self:get_locked_pools() 436 | if not pools then 437 | return nil, err 438 | end 439 | local pool = pools[poolid] 440 | if not pool then 441 | self:unlock_pools() 442 | return nil, 'Pool not found' 443 | end 444 | local host_idx = self.upstream.get_host_idx(hostid, pool.hosts) 445 | if not host_idx then 446 | self:unlock_pools() 447 | return nil, 'Host not found' 448 | end 449 | local host = pool.hosts[host_idx] 450 | 451 | host.up = true 452 | host.lastfail = 0 453 | host.failcount = 0 454 | ngx_log(ngx_DEBUG, 455 | str_format('Host "%s" in Pool "%s" is manually up', 456 | host.id, 457 | poolid 458 | ) 459 | ) 460 | 461 | local ok, err = self:save_pools(pools) 462 | self:unlock_pools() 463 | return ok, err 464 | end 465 | 466 | return _M 467 | -------------------------------------------------------------------------------- /lib/resty/upstream/http.lua: -------------------------------------------------------------------------------- 1 | local ngx_worker_pid = ngx.worker.pid 2 | local ngx_timer_at = ngx.timer.at 3 | local ngx_log = ngx.log 4 | local ngx_ERR = ngx.ERR 5 | local ngx_WARN = ngx.WARN 6 | local ngx_DEBUG = ngx.DEBUG 7 | local ngx_var = ngx.var 8 | local str_lower = string.lower 9 | local str_format = string.format 10 | local str_sub = string.sub 11 | local tostring = tostring 12 | local http = require("resty.http") 13 | 14 | local _M = { 15 | _VERSION = "0.10", 16 | } 17 | 18 | local mt = { __index = _M } 19 | 20 | local default_status_codes = { 21 | --['5xx'] = true, 22 | --['40x'] = true 23 | } 24 | 25 | local defaults = { 26 | read_timeout = 10000, 27 | keepalive_timeout = 60000, 28 | keepalive_pool = 128 29 | } 30 | 31 | local ssl_defaults = { 32 | ssl = false, 33 | ssl_verify = true, 34 | sni_host = nil, 35 | } 36 | 37 | local healthcheck_defaults = { 38 | method = "GET", 39 | path = "/", 40 | headers = { 41 | ["User-Agent"] = "Resty Upstream/".. _M._VERSION.. " HTTP Check (lua)" 42 | } 43 | } 44 | 45 | function _M.new(_, upstream, ssl_opts) 46 | local ssl_opts = setmetatable(ssl_opts or {}, {__index = ssl_defaults}) 47 | local self = { 48 | upstream = upstream, 49 | ssl_opts = ssl_opts 50 | } 51 | return setmetatable(self, mt) 52 | end 53 | 54 | 55 | function _M.log(self, ...) 56 | self.upstream:log(...) 57 | end 58 | 59 | 60 | function _M.bind(self, ...) 61 | return self.upstream:bind(...) 62 | end 63 | 64 | 65 | function _M.process_failed_hosts(self, ...) 66 | self.upstream:process_failed_hosts(...) 67 | end 68 | 69 | 70 | local function failed_request(self, host, poolid) 71 | local upstream = self.upstream 72 | local failed_hosts = upstream:get_failed_hosts(poolid) 73 | failed_hosts[host] = true 74 | end 75 | 76 | 77 | local function http_check_request(self, httpc, params) 78 | local res, err = httpc:request(params) 79 | 80 | -- Read and discard body 81 | local reader 82 | if res then 83 | reader = res.body_reader 84 | end 85 | if reader then 86 | repeat 87 | local chunk, err = reader(65536) 88 | if err then 89 | self:log(ngx_WARN, "Healthcheck read error: "..(err or "")) 90 | break 91 | end 92 | until not chunk 93 | end 94 | 95 | -- Don't use keepalives in background checks 96 | httpc:close() 97 | 98 | return res, err 99 | end 100 | 101 | 102 | local function healthcheck(self, host, pool, host_idx) 103 | -- Only run if healthcheck is configured, has never been checked or is over check interval 104 | local now = ngx.now() 105 | local healthcheck = host.healthcheck 106 | if healthcheck and (healthcheck.interval + (healthcheck.last_check or 0)) <= now then 107 | -- Healthcheck requests could take a long time, lock for as short as possible 108 | local upstream = self.upstream 109 | local locked_pools, err = upstream:get_locked_pools() 110 | if locked_pools then 111 | locked_pools[pool.id].hosts[host_idx].healthcheck.last_check = now -- TODO: sanity checking here 112 | local ok, err = upstream:save_pools(locked_pools) 113 | if not ok then 114 | self:log(ngx_ERR, "Error saving pools: ", err) 115 | end 116 | upstream:unlock_pools() 117 | end 118 | 119 | -- Set default values for healthcheck, resty-http uses metatables internally so do this manually 120 | for k,v in pairs(healthcheck_defaults) do 121 | if not healthcheck[k] then 122 | healthcheck[k] = v 123 | end 124 | end 125 | -- Set default headers 126 | for k,v in pairs(healthcheck_defaults.headers) do 127 | if not healthcheck.headers[k] then 128 | healthcheck.headers[k] = v 129 | end 130 | end 131 | 132 | local httpc = http.new() 133 | -- Set connect timeout 134 | httpc:set_timeout(healthcheck.timeout or pool.timeout) 135 | 136 | local ok,err = httpc:connect(host.host, host.port) 137 | if not ok then 138 | failed_request(self, host.id, pool.id) 139 | if host.up then 140 | -- Only log if it wasn't already down 141 | self:log(ngx_WARN, 142 | str_format("Connection failed for host '%s' (%s:%i) in pool '%s': %s", 143 | host.id, host.host, host.port, pool.id, err) 144 | ) 145 | end 146 | else 147 | -- Set read timeout 148 | local read_timeout = healthcheck.read_timeout or (pool.read_timeout or defaults.read_timeout) 149 | httpc:set_timeout(read_timeout) 150 | 151 | local ssl_opts = self.ssl_opts 152 | local ssl_ok = true 153 | if ssl_opts.ssl then 154 | local err 155 | ssl_ok, err = httpc:ssl_handshake(nil, ssl_opts.sni_name, ssl_opts.verify) 156 | if not ssl_ok then 157 | failed_request(self, host.id, pool.id) 158 | if host.up then 159 | -- Only log if it wasn't already down 160 | self:log(ngx_WARN, 161 | str_format("SSL Handshake failed for host '%s' (%s:%i) in pool '%s': %s", 162 | host.id, host.host, host.port, pool.id, err) 163 | ) 164 | end 165 | end 166 | end 167 | if ssl_ok then -- Don't HTTP if handshake failed 168 | local res, err = http_check_request(self, httpc, healthcheck) 169 | res, err = self:validate_response(res, err, host, pool, healthcheck) 170 | end 171 | end 172 | end 173 | end 174 | 175 | 176 | function _M._http_background_func(self) 177 | -- Active HTTP checks 178 | local spawn = ngx.thread.spawn 179 | local wait = ngx.thread.wait 180 | local threads = {} 181 | local thread_idx = 0 182 | 183 | local upstream = self.upstream 184 | local pools = upstream:get_pools() 185 | for poolid, pool in pairs(pools) do 186 | pool.id = poolid 187 | for host_idx, host in ipairs(pool.hosts) do 188 | thread_idx = thread_idx + 1 189 | threads[thread_idx] = spawn(healthcheck, self, host, pool, host_idx) 190 | end 191 | end 192 | 193 | for i = 1, thread_idx do 194 | local ok, res = wait(threads[i]) 195 | if not ok then 196 | self:log(ngx_ERR, "Thread #", i, ": failed to run: ", res) 197 | end 198 | end 199 | end 200 | 201 | 202 | local http_background_thread 203 | http_background_thread = function(premature, self) 204 | if premature then 205 | self:log(ngx_DEBUG, ngx_worker_pid(), " background thread prematurely exiting") 206 | return 207 | end 208 | local upstream = self.upstream 209 | 210 | -- Call ourselves on a timer again 211 | local ok, err = ngx_timer_at(upstream.background_period, http_background_thread, self) 212 | 213 | if not upstream:get_background_lock() then 214 | return 215 | end 216 | 217 | -- HTTP active checks 218 | self:_http_background_func() 219 | 220 | -- Run process_failed_hosts inline rather than after the request is done 221 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 222 | 223 | -- Revive any hosts that have passed their fail timeout 224 | upstream:revive_hosts() 225 | 226 | upstream:release_background_lock() 227 | end 228 | 229 | 230 | function _M.init_background_thread(self) 231 | local ok, err = ngx_timer_at(1, http_background_thread, self) 232 | if not ok then 233 | self:log(ngx_ERR, "Failed to start background thread: ", err) 234 | end 235 | end 236 | 237 | 238 | function _M.validate_response(self, res, http_err, host, pool, healthcheck) 239 | if not res then 240 | -- Request failed in some fashion 241 | if host.up == true then 242 | self:log(ngx_WARN, 243 | str_format("HTTP Request Error from host '%s' (%s:%i) in pool '%s': %s", 244 | (host.id or "unknown"), 245 | host.host or "unknown", 246 | host.port or 0, 247 | pool.id, 248 | (http_err or "") 249 | )) 250 | end 251 | 252 | -- Mark host down and return 253 | failed_request(self, host.id, pool.id) 254 | 255 | else 256 | -- Got a response, check status 257 | local status_codes = pool.status_codes or default_status_codes 258 | if healthcheck then 259 | status_codes = healthcheck.status_codes or status_codes 260 | end 261 | local status_code = tostring(res.status) 262 | 263 | -- Status codes are always 3 characters, so check for #xx or ##x 264 | if status_codes[status_code] 265 | or status_codes[str_sub(status_code, 1, 1)..'xx'] 266 | or status_codes[str_sub(status_code, 1, 2)..'x'] 267 | then 268 | 269 | res = nil -- Set res to nil so the outer loop re-runs 270 | http_err = status_code 271 | failed_request(self, host.id, pool.id) 272 | 273 | if host.up == true then 274 | self:log(ngx_WARN, 275 | str_format('HTTP %s from Host "%s" (%s:%i) in pool "%s"', 276 | status_code or "nil", 277 | host.id or "nil", 278 | host.host or "nil", 279 | host.port or "nil", 280 | pool.id or "nil" 281 | ) 282 | ) 283 | end 284 | end 285 | end 286 | return res, http_err 287 | end 288 | 289 | 290 | function _M.httpc(self) 291 | local ctx = self.upstream:ctx() 292 | if not ctx.httpc then 293 | ctx.httpc = http.new() 294 | end 295 | return ctx.httpc 296 | end 297 | 298 | 299 | function _M.get_client_body_reader(self, ...) 300 | return self:httpc():get_client_body_reader(...) 301 | end 302 | 303 | 304 | local function _request(self, upstream, httpc, params) 305 | local httpc, conn_info = upstream:connect(httpc) 306 | 307 | if not httpc then 308 | -- Connection err 309 | return nil, conn_info 310 | end 311 | 312 | local host = conn_info.host or {} 313 | local pool = conn_info.pool or {} 314 | 315 | local ssl_opts = self.ssl_opts 316 | 317 | if ssl_opts.ssl then 318 | local host_data = upstream:get_host_operational_data(pool.id, host.id) 319 | local ok, err = httpc:ssl_handshake(host_data.ssl_session, ssl_opts.sni_host or ngx.var.host, ssl_opts.ssl_verify) 320 | if not ok then 321 | self:log(ngx_WARN, 322 | str_format("SSL Error connecting to '%s' (%s:%d): %s", host.id, host.host, host.port, err)) 323 | failed_request(self, host.id, pool.id) 324 | return ok, err 325 | end 326 | host_data.ssl_session = ok 327 | end 328 | 329 | httpc:set_timeout(pool.read_timeout or defaults.read_timeout) 330 | 331 | local res, http_err = httpc:request(params) 332 | res, http_err = self:validate_response(res, http_err, host, pool) 333 | 334 | if not res then 335 | return nil, http_err 336 | end 337 | return res, conn_info 338 | end 339 | 340 | 341 | function _M.request(self, params) 342 | local httpc = self:httpc() 343 | local upstream = self.upstream 344 | 345 | local body_reusable = (type(params.body) ~= 'function') 346 | local prev_err 347 | repeat 348 | local res, err = _request(self, upstream, httpc, params) 349 | if res then 350 | self.upstream:ctx().conn_info = err 351 | return res, err 352 | else 353 | -- Either connect or http failed to all available hosts 354 | if err == "No available upstream hosts" or not body_reusable then 355 | if prev_err then 356 | -- Got a connection at some point but bad HTTP 357 | return nil, prev_err, 502 358 | elseif not body_reusable then 359 | -- Bad HTTP response but can't resend the body another host 360 | return nil, err, 502 361 | else 362 | -- No connections at all 363 | return nil, err, 504 364 | end 365 | end 366 | prev_err = err 367 | end 368 | until res 369 | end 370 | 371 | 372 | function _M.set_keepalive(self, ...) 373 | local keepalive_timeout, keepalive_pool 374 | local conn_info = self.upstream:ctx().conn_info 375 | if not conn_info or not conn_info.pool then 376 | keepalive_timeout = select(1, ...) or defaults.keepalive_timeout 377 | keepalive_pool = select(2, ...) or defaults.keepalive_pool 378 | else 379 | local pool = conn_info.pool 380 | keepalive_timeout = pool.keepalive_timeout 381 | keepalive_pool = pool.keepalive_pool 382 | end 383 | 384 | return self:httpc():set_keepalive(keepalive_timeout, keepalive_pool) 385 | end 386 | 387 | 388 | function _M.get_reused_times(self, ...) 389 | return self:httpc():get_reused_times(...) 390 | end 391 | 392 | 393 | function _M.close(self, ...) 394 | return self:httpc():close(...) 395 | end 396 | 397 | return _M 398 | -------------------------------------------------------------------------------- /lib/resty/upstream/socket.lua: -------------------------------------------------------------------------------- 1 | local ngx_socket_tcp = ngx.socket.tcp 2 | local ngx_timer_at = ngx.timer.at 3 | local ngx_worker_pid = ngx.worker.pid 4 | local ngx_log = ngx.log 5 | local ngx_DEBUG = ngx.DEBUG 6 | local ngx_ERR = ngx.ERR 7 | local ngx_WARN = ngx.WARN 8 | local ngx_INFO = ngx.INFO 9 | local str_format = string.format 10 | local tbl_insert = table.insert 11 | local tbl_sort = table.sort 12 | local now = ngx.now 13 | local pairs = pairs 14 | local ipairs = ipairs 15 | local getfenv = getfenv 16 | local shared = ngx.shared 17 | local phase = ngx.get_phase 18 | local cjson = require('cjson') 19 | local cjson_encode = cjson.encode 20 | local cjson_decode = cjson.decode 21 | local resty_lock = require('resty.lock') 22 | 23 | -- from resty-core v0.1.16, use 'get_request()' to replace 'getfenv(0).__ngx_req' 24 | local get_request = require('resty.core.base').get_request 25 | if not get_request then 26 | get_request = function() return getfenv(0).__ngx_req end 27 | end 28 | 29 | local safe_json = function(func, data) 30 | local ok, ret = pcall(func, data) 31 | if ok then 32 | return ret 33 | else 34 | ngx_log(ngx_ERR, ret) 35 | return nil, ret 36 | end 37 | end 38 | 39 | 40 | local json_decode = function(data) 41 | return safe_json(cjson_decode, data) 42 | end 43 | 44 | 45 | local json_encode = function(data) 46 | return safe_json(cjson_encode, data) 47 | end 48 | 49 | 50 | local _M = { 51 | _VERSION = "0.10", 52 | available_methods = {}, 53 | background_period = 10, 54 | background_timeout = 120 55 | } 56 | 57 | local mt = { __index = _M } 58 | 59 | 60 | local event_types = { 61 | host_up = true, 62 | host_down = true, 63 | } 64 | 65 | local background_thread 66 | background_thread = function(premature, self) 67 | if premature then 68 | self:log(ngx_DEBUG, ngx_worker_pid(), " background thread prematurely exiting") 69 | return 70 | end 71 | -- Call ourselves on a timer again 72 | local ok, err = ngx_timer_at(self.background_period, background_thread, self) 73 | 74 | if not self:get_background_lock() then 75 | return 76 | end 77 | 78 | self:revive_hosts() 79 | 80 | self:release_background_lock() 81 | end 82 | 83 | 84 | function _M.log(self, level, ...) 85 | ngx_log(level, "Upstream '", self.id,"': ", ...) 86 | end 87 | 88 | 89 | function _M.get_background_lock(self) 90 | local pid = ngx_worker_pid() 91 | local dict = self.dict 92 | local lock, err = dict:add(self.background_flag, pid, self.background_timeout) 93 | if lock then 94 | return true 95 | end 96 | if err == 'exists' then 97 | return false 98 | else 99 | self:log(ngx_DEBUG, "Could not add background lock key in pid #", pid) 100 | return false 101 | end 102 | end 103 | 104 | 105 | function _M.release_background_lock(self) 106 | local dict = self.dict 107 | local pid, err = dict:get(self.background_flag) 108 | if not pid then 109 | self:log(ngx_ERR, "Failed to get key '", self.background_flag, "': ", err) 110 | return 111 | end 112 | if pid == ngx_worker_pid() then 113 | local ok, err = dict:delete(self.background_flag) 114 | if not ok then 115 | self:log(ngx_ERR, "Failed to delete key '", self.background_flag, "': ", err) 116 | end 117 | end 118 | end 119 | 120 | 121 | function _M.new(_, dict_name, id) 122 | local dict = shared[dict_name] 123 | if not dict then 124 | ngx_log(ngx_ERR, "Shared dictionary not found" ) 125 | return nil 126 | end 127 | 128 | if not id then id = 'default_upstream' end 129 | if type(id) ~= 'string' then 130 | return nil, 'Upstream ID must be a string' 131 | end 132 | 133 | local self = { 134 | id = id, 135 | dict = dict, 136 | dict_name = dict_name, 137 | listeners = {}, 138 | 139 | -- Per worker data 140 | operational_data = {}, 141 | } 142 | -- Create unique dictionary keys for this instance of upstream 143 | self.pools_key = self.id..'_pools' 144 | self.priority_key = self.id..'_priority_index' 145 | self.background_flag = self.id..'_background_running' 146 | self.lock_key = self.id..'_lock' 147 | 148 | local configured = true 149 | if dict:get(self.pools_key) == nil then 150 | dict:set(self.pools_key, json_encode({})) 151 | configured = false 152 | end 153 | 154 | return setmetatable(self, mt), configured 155 | end 156 | 157 | 158 | function _M.bind(self, event, func) 159 | if not event_types[event] then 160 | return nil, "Event not found" 161 | end 162 | if type(func) ~= 'function' then 163 | return nil, "Can only bind a function" 164 | end 165 | 166 | local listeners = self.listeners[event] 167 | if not listeners then 168 | self.listeners[event] = {} 169 | listeners = self.listeners[event] 170 | end 171 | tbl_insert(listeners, func) 172 | return true 173 | end 174 | 175 | 176 | local function emit(self, event, data) 177 | local listeners = self.listeners[event] 178 | if not listeners then 179 | return 180 | end 181 | for _, func in ipairs(listeners) do 182 | local ok, err = pcall(func,data) 183 | if not ok then 184 | self:log(ngx_ERR, "Error running listener, event '", event,"': ", err) 185 | end 186 | end 187 | end 188 | 189 | 190 | -- A safe place in ngx.ctx for the current module instance (self). 191 | function _M.ctx(self) 192 | -- No request available so must be the init phase, return an empty table 193 | if not get_request() then 194 | return {} 195 | end 196 | 197 | local ngx_ctx = ngx.ctx 198 | local id = self.id 199 | local ctx = ngx_ctx[id] 200 | if ctx == nil then 201 | ctx = { 202 | failed = {} 203 | } 204 | ngx_ctx[id] = ctx 205 | end 206 | return ctx 207 | end 208 | 209 | 210 | function _M.get_pools(self) 211 | local ctx = self:ctx() 212 | local err 213 | if ctx.pools == nil then 214 | local pool_str = self.dict:get(self.pools_key) 215 | if not pool_str then 216 | return nil, err 217 | end 218 | local pools, err = json_decode(pool_str) 219 | if not pools then 220 | return nil, err 221 | end 222 | ctx.pools = json_decode(pool_str) 223 | end 224 | return ctx.pools 225 | end 226 | 227 | 228 | local function get_lock_obj(self) 229 | local ctx = self:ctx() 230 | if not ctx.lock then 231 | ctx.lock = resty_lock:new(self.dict_name) 232 | end 233 | return ctx.lock 234 | end 235 | 236 | 237 | function _M.get_locked_pools(self) 238 | if phase() == 'init' then 239 | return self:get_pools() 240 | end 241 | local lock = get_lock_obj(self) 242 | local ok, err = lock:lock(self.lock_key) 243 | 244 | if ok then 245 | local pool_str, err = self.dict:get(self.pools_key) 246 | if not pool_str then 247 | return nil, err 248 | end 249 | local pools, err = json_decode(pool_str) 250 | return pools, err 251 | else 252 | self:log(ngx_INFO, "Failed to lock pools: ", err) 253 | end 254 | 255 | return ok, err 256 | end 257 | 258 | 259 | function _M.unlock_pools(self) 260 | if phase() == 'init' then 261 | return true 262 | end 263 | local lock = get_lock_obj(self) 264 | local ok, err = lock:unlock(self.lock_key) 265 | if not ok then 266 | self:log(ngx_ERR, "Failed to release pools lock: ", err) 267 | end 268 | return ok, err 269 | end 270 | 271 | 272 | function _M.get_priority_index(self) 273 | local ctx = self:ctx() 274 | if ctx.priority_index == nil then 275 | local priority_str, err = self.dict:get(self.priority_key) 276 | if not priority_str then 277 | return nil, err 278 | end 279 | local priority_index, err = json_decode(priority_str) 280 | if not priority_index then 281 | return nil, err 282 | end 283 | ctx.priority_index = priority_index 284 | end 285 | return ctx.priority_index 286 | end 287 | 288 | 289 | local function _gcd(a,b) 290 | -- Tail recursive gcd function 291 | if b == 0 then 292 | return a 293 | else 294 | return _gcd(b, a % b) 295 | end 296 | end 297 | 298 | 299 | local function calc_gcd_weight(hosts) 300 | -- Calculate the GCD and maximum weight value from a set of hosts 301 | local gcd = 0 302 | local len = #hosts - 1 303 | local max_weight = 0 304 | local i = 1 305 | 306 | if len < 1 then 307 | return 0, 0 308 | end 309 | 310 | repeat 311 | local tmp = _gcd(hosts[i].weight, hosts[i+1].weight) 312 | if tmp > gcd then 313 | gcd = tmp 314 | end 315 | if hosts[i].weight > max_weight then 316 | max_weight = hosts[i].weight 317 | end 318 | i = i +1 319 | until i >= len 320 | if hosts[i].weight > max_weight then 321 | max_weight = hosts[i].weight 322 | end 323 | 324 | return gcd, max_weight 325 | end 326 | 327 | 328 | function _M.save_pools(self, pools) 329 | pools = pools or {} 330 | self:ctx().pools = pools 331 | local serialised, err = json_encode(pools) 332 | if not serialised then 333 | return nil, err 334 | end 335 | return self.dict:set(self.pools_key, serialised) 336 | end 337 | 338 | 339 | function _M.sort_pools(self, pools) 340 | -- Create a table of priorities and a map back to the pool 341 | local priorities = {} 342 | local map = {} 343 | for id,p in pairs(pools) do 344 | map[p.priority] = id 345 | tbl_insert(priorities, p.priority) 346 | end 347 | tbl_sort(priorities) 348 | 349 | local sorted_pools = {} 350 | for k,pri in ipairs(priorities) do 351 | tbl_insert(sorted_pools, map[pri]) 352 | end 353 | 354 | local serialised = json_encode(sorted_pools) 355 | return self.dict:set(self.priority_key, serialised) 356 | end 357 | 358 | 359 | function _M.init_background_thread(self) 360 | local ok, err = ngx_timer_at(1, background_thread, self) 361 | if not ok then 362 | self:log(ngx_ERR, "Failed to start background thread: ", err) 363 | end 364 | end 365 | 366 | 367 | function _M.revive_hosts(self) 368 | local now = now() 369 | 370 | -- Reset state for any failed hosts 371 | local pools, err = self:get_locked_pools() 372 | if not pools then 373 | return nil, err 374 | end 375 | 376 | local changed = false 377 | for poolid,pool in pairs(pools) do 378 | local failed_timeout = pool.failed_timeout 379 | local max_fails = pool.max_fails 380 | for k, host in ipairs(pool.hosts) do 381 | -- Reset any hosts past their timeout 382 | if host.lastfail ~= 0 and (host.lastfail + failed_timeout) < now then 383 | host.failcount = 0 384 | host.lastfail = 0 385 | changed = true 386 | if not host.up then 387 | host.up = true 388 | self:log(ngx_INFO, 389 | str_format('Host "%s" in Pool "%s" is up', host.id, poolid) 390 | ) 391 | pool.id = poolid 392 | emit(self, "host_up", {host = host, pool = pool}) 393 | end 394 | end 395 | end 396 | end 397 | 398 | local ok, err = true, nil 399 | if changed then 400 | ok, err = self:save_pools(pools) 401 | if not ok then 402 | self:log(ngx_ERR, "Error saving pools: ", err) 403 | end 404 | end 405 | self:unlock_pools() 406 | return ok, err 407 | end 408 | 409 | 410 | function _M.get_host_idx(id, hosts) 411 | for i, host in ipairs(hosts) do 412 | if host.id == id then 413 | return i 414 | end 415 | end 416 | return nil 417 | end 418 | 419 | 420 | function _M._process_failed_hosts(premature, self, ctx) 421 | local failed = ctx.failed 422 | local now = now() 423 | local get_host_idx = self.get_host_idx 424 | local pools, err = self:get_locked_pools() 425 | if not pools then 426 | return 427 | end 428 | 429 | local changed = false 430 | for poolid,hosts in pairs(failed) do 431 | local pool = pools[poolid] 432 | local failed_timeout = pool.failed_timeout 433 | local max_fails = pool.max_fails 434 | local pool_hosts = pool.hosts 435 | 436 | for id,_ in pairs(hosts) do 437 | local host_idx = get_host_idx(id, pool_hosts) 438 | local host = pool_hosts[host_idx] 439 | 440 | changed = true 441 | host.lastfail = now 442 | host.failcount = host.failcount + 1 443 | if host.failcount >= max_fails and host.up == true then 444 | host.up = false 445 | self:log(ngx_WARN, 446 | str_format('Host "%s" in Pool "%s" is down', host.id, poolid) 447 | ) 448 | pool.id = poolid 449 | emit(self, "host_down", {host = host, pool = pool}) 450 | end 451 | end 452 | end 453 | 454 | local ok, err = true, nil 455 | if changed then 456 | ok, err = self:save_pools(pools) 457 | if not ok then 458 | self:log(ngx_ERR, "Error saving pools: ", err) 459 | end 460 | end 461 | 462 | self:unlock_pools() 463 | return ok, err 464 | end 465 | 466 | 467 | function _M.process_failed_hosts(self) 468 | -- Run in a background thread immediately after the request is done 469 | ngx_timer_at(0, self._process_failed_hosts, self, self:ctx()) 470 | end 471 | 472 | 473 | function _M.get_host_operational_data(self, poolid, hostid) 474 | local op_data = self.operational_data 475 | local pool_data = op_data[poolid] 476 | if not pool_data then 477 | op_data[poolid] = { hosts = {} } 478 | pool_data = op_data[poolid] 479 | end 480 | 481 | local pool_hosts_data = pool_data['hosts'] 482 | if not pool_hosts_data then 483 | pool_data['hosts'] = {} 484 | pool_hosts_data = pool_data['hosts'] 485 | end 486 | 487 | local host_data = pool_hosts_data[hostid] 488 | if not host_data then 489 | pool_hosts_data[hostid] = {} 490 | host_data = pool_hosts_data[hostid] 491 | end 492 | 493 | return host_data 494 | end 495 | 496 | 497 | function _M.get_failed_hosts(self, poolid) 498 | local f = self:ctx().failed 499 | local failed_hosts = f[poolid] 500 | if not failed_hosts then 501 | f[poolid] = {} 502 | failed_hosts = f[poolid] 503 | end 504 | return failed_hosts 505 | end 506 | 507 | 508 | function _M.connect_failed(self, host, poolid, failed_hosts) 509 | -- Flag host as failed 510 | local hostid = host.id 511 | failed_hosts[hostid] = true 512 | self:log(ngx_WARN, 513 | str_format('Failed connecting to Host "%s" (%s:%d) from pool "%s"', 514 | hostid, 515 | host.host, 516 | host.port, 517 | poolid 518 | ) 519 | ) 520 | end 521 | 522 | 523 | local function get_hash_host(vars) 524 | local h = vars.hash 525 | local hosts = vars.available_hosts 526 | local weight_sum = vars.weight_sum 527 | local hostcount = #hosts 528 | 529 | if hostcount == 0 then return end 530 | 531 | local cur_idx = 1 532 | 533 | -- figure where we should go 534 | local cur_weight = hosts[cur_idx].weight 535 | 536 | while (h >= cur_weight) do 537 | h = h - cur_weight 538 | 539 | if (h < 0) then 540 | h = maxweight + h 541 | end 542 | 543 | cur_idx = cur_idx + 1 544 | 545 | if (cur_idx > hostcount) then 546 | cur_idx = 1 547 | end 548 | 549 | cur_weight = hosts[cur_idx].weight 550 | end 551 | 552 | -- now cur_idx points us to where we should go 553 | return hosts[cur_idx] 554 | end 555 | 556 | 557 | local function get_hash_vars(hosts, failed_hosts, key) 558 | local available_hosts = {} -- new tab needed here 559 | local n = 0 560 | local weight_sum = 0 561 | 562 | for i=1, #hosts do 563 | local host = hosts[i] 564 | 565 | if (host.up and not failed_hosts[host.id]) then 566 | n = n + 1 567 | available_hosts[n] = host 568 | weight_sum = weight_sum + host.weight 569 | end 570 | end 571 | 572 | local hash = ngx.crc32_short(key) % weight_sum 573 | 574 | return { 575 | available_hosts = available_hosts, 576 | weight_sum = weight_sum, 577 | hash = hash, 578 | } 579 | end 580 | 581 | 582 | _M.available_methods.hash = function(self, pool, sock, key) 583 | local hosts = pool.hosts 584 | local poolid = pool.id 585 | local hash_key = key or ngx.var.remote_addr 586 | 587 | local failed_hosts = self:get_failed_hosts(poolid) 588 | 589 | -- Attempt a connection 590 | if #hosts == 1 then 591 | -- Don't bother trying to balance between 1 host 592 | local host = hosts[1] 593 | if host.up == false or failed_hosts[host.id] then 594 | return nil, sock, {}, nil 595 | end 596 | local connected, err = sock:connect(host.host, host.port) 597 | if not connected then 598 | self:connect_failed(host, poolid, failed_hosts) 599 | end 600 | return connected, sock, host, err 601 | end 602 | 603 | local hash_vars = get_hash_vars(hosts, failed_hosts, hash_key) 604 | 605 | local connected, err 606 | repeat 607 | local host = get_hash_host(hash_vars) 608 | if not host then 609 | -- Ran out of hosts, break out of the loop (go to next pool) 610 | break 611 | end 612 | 613 | -- Try connecting to the selected host 614 | connected, err = sock:connect(host.host, host.port) 615 | 616 | if connected then 617 | return connected, sock, host, err 618 | else 619 | -- Mark the host bad and retry 620 | self:connect_failed(host, poolid, failed_hosts) 621 | 622 | -- rehash 623 | hash_vars = get_hash_vars(hosts, failed_hosts, hash_key) 624 | end 625 | until connected 626 | -- All hosts have failed 627 | return nil, sock, {}, err 628 | end 629 | 630 | 631 | local function select_weighted_rr_host(hosts, failed_hosts, round_robin_vars) 632 | local idx = round_robin_vars.idx 633 | local cw = round_robin_vars.cw 634 | local gcd = round_robin_vars.gcd 635 | local max_weight = round_robin_vars.max_weight 636 | 637 | local hostcount = #hosts 638 | local failed_iters = 0 639 | repeat 640 | idx = idx +1 641 | if idx > hostcount then 642 | idx = 1 643 | end 644 | if idx == 1 then 645 | cw = cw - gcd 646 | if cw <= 0 then 647 | cw = max_weight 648 | if cw == 0 then 649 | return nil 650 | end 651 | end 652 | end 653 | local host = hosts[idx] 654 | if host.weight >= cw then 655 | if failed_hosts[host.id] == nil and host.up == true then 656 | round_robin_vars.idx, round_robin_vars.cw = idx, cw 657 | return host, idx 658 | else 659 | failed_iters = failed_iters+1 660 | end 661 | end 662 | until failed_iters > hostcount -- Checked every host, must all be down 663 | return 664 | end 665 | 666 | 667 | local function get_round_robin_vars(self, pool) 668 | local operational_data = self.operational_data 669 | local pool_data = operational_data[pool.id] 670 | if not pool_data then 671 | operational_data[pool.id] = { hosts = {}, round_robin = {idx = 0, cw = 0} } 672 | pool_data = operational_data[pool.id] 673 | end 674 | 675 | local round_robin_vars = pool_data["round_robin"] 676 | if not round_robin_vars then 677 | pool_data["round_robin"] = {idx = 0, cw = 0} 678 | round_robin_vars = pool_data["round_robin"] 679 | end 680 | 681 | round_robin_vars.gcd, round_robin_vars.max_weight = calc_gcd_weight(pool.hosts) 682 | return round_robin_vars 683 | end 684 | 685 | 686 | _M.available_methods.round_robin = function(self, pool, sock) 687 | local hosts = pool.hosts 688 | local poolid = pool.id 689 | 690 | local failed_hosts = self:get_failed_hosts(poolid) 691 | 692 | -- Attempt a connection 693 | if #hosts == 1 then 694 | -- Don't bother trying to balance between 1 host 695 | local host = hosts[1] 696 | if host.up == false or failed_hosts[host.id] then 697 | return nil, sock, {}, nil 698 | end 699 | local connected, err = sock:connect(host.host, host.port) 700 | if not connected then 701 | self:connect_failed(host, poolid, failed_hosts) 702 | end 703 | return connected, sock, host, err 704 | end 705 | 706 | local round_robin_vars = get_round_robin_vars(self, pool) 707 | 708 | -- Loop until we run out of hosts or have connected 709 | local connected, err 710 | repeat 711 | local host, idx = select_weighted_rr_host(hosts, failed_hosts, round_robin_vars) 712 | if not host then 713 | -- Ran out of hosts, break out of the loop (go to next pool) 714 | break 715 | end 716 | 717 | -- Try connecting to the selected host 718 | connected, err = sock:connect(host.host, host.port) 719 | 720 | if connected then 721 | return connected, sock, host, err 722 | else 723 | -- Mark the host bad and retry 724 | self:connect_failed(host, poolid, failed_hosts) 725 | end 726 | until connected 727 | -- All hosts have failed 728 | return nil, sock, {}, err 729 | end 730 | 731 | 732 | function _M.connect(self, sock, key) 733 | -- Get pool data 734 | local priority_index, err = self:get_priority_index() 735 | if not priority_index then 736 | return nil, 'No valid pool order: '.. (err or "") 737 | end 738 | 739 | local pools, err = self:get_pools() 740 | if not pools then 741 | return nil, 'No valid pool data: '.. (err or "") 742 | end 743 | 744 | -- A socket (or resty client module) can be passed in, otherwise create a socket 745 | if not sock then 746 | sock = ngx_socket_tcp() 747 | end 748 | 749 | -- Resty modules use set_timeout instead 750 | local set_timeout = sock.settimeout or sock.set_timeout 751 | 752 | -- Upvalue these to return errors later 753 | local connected, err, host 754 | local available_methods = self.available_methods 755 | 756 | -- Loop over pools in priority order 757 | for _, poolid in ipairs(priority_index) do 758 | local pool = pools[poolid] 759 | if not pool then 760 | self:log(ngx_ERR, "Pool '", poolid, "' invalid") 761 | else 762 | if pool.up then 763 | pool.id = poolid 764 | -- Set connection timeout 765 | set_timeout(sock, pool.timeout) 766 | 767 | -- Load balance between available hosts using specified method 768 | connected, sock, host, err = available_methods[pool.method](self, pool, sock, key) 769 | 770 | if connected then 771 | -- Return connected socket! 772 | self:log(ngx_DEBUG, str_format("Connected to host '%s' (%s:%i) in pool '%s'", 773 | host.id, host.host, host.port, poolid)) 774 | return sock, {host = host, pool = pool} 775 | end 776 | end 777 | end 778 | end 779 | -- Didnt find any pools with working hosts, return the last error message 780 | return nil, "No available upstream hosts" 781 | end 782 | 783 | return _M 784 | -------------------------------------------------------------------------------- /lua-resty-upstream-0.08-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-upstream" 2 | version = "0.08-0" 3 | source = { 4 | url = "git://github.com/hamishforbes/lua-resty-upstream", 5 | tag = "v0.08" 6 | } 7 | description = { 8 | summary = "Upstream connection load balancing and failover module for Openresty", 9 | homepage = "https://github.com/hamishforbes/lua-resty-upstream", 10 | license = "MIT", 11 | maintainer = "Hamish Forbes" 12 | } 13 | dependencies = { 14 | "lua >= 5.1", 15 | "lua-resty-http >= 0.09", 16 | } 17 | build = { 18 | type = "builtin", 19 | modules = { 20 | ["resty.upstream.socket"] = "lib/resty/upstream/socket.lua", 21 | ["resty.upstream.http"] = "lib/resty/upstream/http.lua", 22 | ["resty.upstream.api"] = "lib/resty/upstream/api.lua", 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /t/01-socket.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * (26); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | init_by_lua ' 17 | cjson = require "cjson" 18 | upstream_socket = require("resty.upstream.socket") 19 | 20 | upstream, configured = upstream_socket:new("test_upstream") 21 | 22 | local pools = { 23 | primary = { 24 | up = true, 25 | method = "round_robin", 26 | timeout = 100, 27 | priority = 0, 28 | hosts = { 29 | web01 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true }, 30 | web02 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true } 31 | } 32 | }, 33 | tertiary = { 34 | up = true, 35 | method = "round_robin", 36 | timeout = 2000, 37 | priority = 15, 38 | hosts = { 39 | { host = "10.10.10.1", weight = 10, port = "81", lastfail = 0, failcount = 0, up = true } 40 | 41 | } 42 | }, 43 | secondary = { 44 | up = true, 45 | method = "round_robin", 46 | timeout = 2000, 47 | priority = 10, 48 | hosts = { 49 | dr01 = { host = "10.10.10.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true } 50 | 51 | } 52 | } 53 | } 54 | 55 | upstream:save_pools(pools) 56 | upstream:sort_pools(pools) 57 | 58 | '; 59 | }; 60 | 61 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 62 | 63 | no_long_string(); 64 | #no_diff(); 65 | 66 | run_tests(); 67 | 68 | __DATA__ 69 | === TEST 1: Dictionary gets set from init_by_lua. 70 | --- http_config eval: $::HttpConfig 71 | --- config 72 | location = /a { 73 | content_by_lua ' 74 | local keys = ngx.shared["test_upstream"]:get_keys() 75 | 76 | if #keys > 0 then 77 | ngx.status = 200 78 | ngx.say("OK") 79 | ngx.exit(200) 80 | else 81 | ngx.status = 500 82 | ngx.say("ERR") 83 | ngx.exit(500) 84 | end 85 | '; 86 | } 87 | --- request 88 | GET /a 89 | --- response_body 90 | OK 91 | --- no_error_log 92 | [error] 93 | [warn] 94 | 95 | === TEST 2: Dictionary can be unserialised. 96 | --- http_config eval: $::HttpConfig 97 | --- config 98 | location = /a { 99 | content_by_lua ' 100 | require "cjson" 101 | 102 | local dict = ngx.shared["test_upstream"] 103 | 104 | local pool_str = dict:get(upstream.pools_key) 105 | local pools = cjson.decode(pool_str) 106 | 107 | local priority_str = dict:get(upstream.priority_key) 108 | local priority_index = cjson.decode(priority_str) 109 | 110 | local fail = true 111 | for k,v in pairs(pools) do 112 | fail = false 113 | end 114 | 115 | if fail then 116 | ngx.status = 500; ngx.say("FAIL"); ngx.exit(500) 117 | end 118 | 119 | local fail = true 120 | for k,v in pairs(priority_index) do 121 | fail = false 122 | end 123 | 124 | if fail then 125 | ngx.status = 500; ngx.say("FAIL"); ngx.exit(500) 126 | end 127 | ngx.say("OK") 128 | '; 129 | } 130 | --- request 131 | GET /a 132 | --- response_body 133 | OK 134 | --- no_error_log 135 | [error] 136 | [warn] 137 | 138 | === TEST 3: Pool Priority sorting 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location = /a { 142 | content_by_lua ' 143 | local dict = ngx.shared["test_upstream"] 144 | 145 | local priority_str = dict:get(upstream.priority_key) 146 | local priority_index = cjson.decode(priority_str) 147 | 148 | for k,v in ipairs(priority_index) do 149 | ngx.say(v) 150 | end 151 | '; 152 | } 153 | --- request 154 | GET /a 155 | --- response_body 156 | primary 157 | secondary 158 | tertiary 159 | --- no_error_log 160 | [error] 161 | [warn] 162 | 163 | === TEST 4: Multiple upstream instances in the same dictionary 164 | --- http_config eval: $::HttpConfig 165 | --- config 166 | location = /a { 167 | content_by_lua ' 168 | local upstream2, configured = upstream_socket:new("test_upstream", "upstream2") 169 | local pools = { 170 | primary = { 171 | up = true, 172 | method = "round_robin", 173 | timeout = 100, 174 | priority = 0, 175 | hosts = { 176 | web01 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true }, 177 | web02 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true } 178 | } 179 | }, 180 | alternate = { 181 | up = true, 182 | method = "round_robin", 183 | timeout = 100, 184 | priority = 0, 185 | hosts = { 186 | web01 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true }, 187 | web02 = { host = "127.0.0.1", weight = 10, port = "80", lastfail = 0, failcount = 0, up = true } 188 | } 189 | } 190 | } 191 | upstream2:save_pools(pools) 192 | 193 | local original = upstream:get_pools() 194 | local alt = upstream2:get_pools() 195 | 196 | local sorted = {} 197 | for k,v in pairs(original) do 198 | table.insert(sorted, k) 199 | end 200 | table.sort(sorted) 201 | for _,v in ipairs(sorted) do 202 | ngx.say(v) 203 | end 204 | 205 | local sorted = {} 206 | for k,v in pairs(alt) do 207 | table.insert(sorted, k) 208 | end 209 | table.sort(sorted) 210 | for _, v in ipairs(sorted) do 211 | ngx.say(v) 212 | end 213 | '; 214 | } 215 | --- request 216 | GET /a 217 | --- response_body 218 | primary 219 | secondary 220 | tertiary 221 | alternate 222 | primary 223 | --- no_error_log 224 | [error] 225 | [warn] 226 | 227 | === TEST 5: Bad upstream ID is rejected 228 | --- http_config eval: $::HttpConfig 229 | --- config 230 | location = /a { 231 | content_by_lua ' 232 | local test, configured = upstream_socket:new("test_upstream", {}) 233 | if test ~= nil then 234 | ngx.status = 500 235 | ngx.exit(500) 236 | else 237 | ngx.say("OK") 238 | end 239 | '; 240 | } 241 | --- request 242 | GET /a 243 | --- response_body 244 | OK 245 | --- no_error_log 246 | [error] 247 | [warn] 248 | 249 | === TEST 6: Bad json encode is caught 250 | --- http_config eval: $::HttpConfig 251 | --- config 252 | location = /a { 253 | content_by_lua ' 254 | local bad_conf = { 255 | primary = function(test) ngx.say("cant serialise a function!") end 256 | } 257 | 258 | local ok, err = upstream:save_pools(bad_conf) 259 | ngx.say(err) 260 | '; 261 | } 262 | --- request 263 | GET /a 264 | --- response_body 265 | Cannot serialise function: type not supported 266 | 267 | --- error_log: Cannot serialise function 268 | 269 | === TEST 7: Bad json decode is caught 270 | --- http_config eval: $::HttpConfig 271 | --- config 272 | location = /a { 273 | content_by_lua ' 274 | local bad_json = [[ 275 | { wtf kind of json is this??!!!11eleven} 276 | ]] 277 | 278 | local dict = ngx.shared["test_upstream"] 279 | dict:set(upstream.pools_key, bad_json) 280 | 281 | local ok, err = upstream:get_pools(bad_conf) 282 | ngx.say(err) 283 | '; 284 | } 285 | --- request 286 | GET /a 287 | --- response_body 288 | Expected object key string but found invalid token at character 15 289 | 290 | --- error_log: Expected object key string but found invalid token 291 | -------------------------------------------------------------------------------- /t/02-pools.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => 6; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | init_by_lua ' 17 | cjson = require "cjson" 18 | upstream_socket = require("resty.upstream.socket") 19 | upstream_api = require("resty.upstream.api") 20 | 21 | upstream, configured = upstream_socket:new("test_upstream") 22 | test_api = upstream_api:new(upstream) 23 | 24 | test_api:create_pool({id = "primary", timeout = 10}) 25 | 26 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10}) 27 | '; 28 | }; 29 | 30 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 31 | 32 | no_long_string(); 33 | #no_diff(); 34 | 35 | run_tests(); 36 | 37 | __DATA__ 38 | === TEST 1: Failover to secondary pool 39 | --- http_config eval: $::HttpConfig 40 | --- log_level: debug 41 | --- config 42 | location = /a { 43 | content_by_lua ' 44 | -- Bad hosts 45 | test_api:add_host("primary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 46 | test_api:add_host("primary", { id="b", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 47 | -- Good hosts 48 | test_api:add_host("secondary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 10 }) 49 | test_api:add_host("secondary", { id="b", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 10 }) 50 | 51 | local sock, err = upstream:connect() 52 | if not sock then 53 | ngx.log(ngx.ERR, err) 54 | ngx.say(err) 55 | else 56 | sock:close() 57 | ngx.say(err.pool.id) 58 | end 59 | '; 60 | } 61 | --- request 62 | GET /a 63 | --- response_body 64 | secondary 65 | 66 | === TEST 2: Failover to secondary pool, with 1 host in primary 67 | --- http_config eval: $::HttpConfig 68 | --- log_level: debug 69 | --- config 70 | location = /a { 71 | content_by_lua ' 72 | -- Bad hosts 73 | test_api:add_host("primary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 74 | -- Good hosts 75 | test_api:add_host("secondary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 10 }) 76 | test_api:add_host("secondary", { id="b", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 10 }) 77 | 78 | local sock, err = upstream:connect() 79 | if not sock then 80 | ngx.log(ngx.ERR, err) 81 | ngx.say(err) 82 | else 83 | sock:close() 84 | ngx.say(err.pool.id) 85 | end 86 | '; 87 | } 88 | --- request 89 | GET /a 90 | --- response_body 91 | secondary 92 | 93 | === TEST 3: Failover to secondary pool, with 1 host in both 94 | --- http_config eval: $::HttpConfig 95 | --- log_level: debug 96 | --- config 97 | location = /a { 98 | content_by_lua ' 99 | -- Bad hosts 100 | test_api:add_host("primary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 101 | -- Good hosts 102 | test_api:add_host("secondary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 10 }) 103 | 104 | local sock, err = upstream:connect() 105 | if not sock then 106 | ngx.log(ngx.ERR, err) 107 | ngx.say(err) 108 | else 109 | sock:close() 110 | ngx.say(err.pool.id) 111 | end 112 | '; 113 | } 114 | --- request 115 | GET /a 116 | --- response_body 117 | secondary 118 | -------------------------------------------------------------------------------- /t/03-hosts.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 15; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | 17 | }; 18 | 19 | our $InitConfig = qq{ 20 | init_by_lua ' 21 | cjson = require "cjson" 22 | upstream_socket = require("resty.upstream.socket") 23 | upstream_api = require("resty.upstream.api") 24 | 25 | upstream, configured = upstream_socket:new("test_upstream") 26 | test_api = upstream_api:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100}) 29 | 30 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10}) 31 | }; 32 | 33 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 34 | 35 | no_long_string(); 36 | #no_diff(); 37 | 38 | run_tests(); 39 | 40 | __DATA__ 41 | === TEST 1: Connecting to a single host 42 | --- http_config eval 43 | "$::HttpConfig" 44 | ."$::InitConfig" 45 | . q{ 46 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 47 | '; 48 | } 49 | --- log_level: debug 50 | --- config 51 | location = / { 52 | content_by_lua ' 53 | 54 | local ok, err = upstream:connect() 55 | if ok then 56 | ngx.say("OK") 57 | else 58 | ngx.say(cjson.encode(err)) 59 | end 60 | '; 61 | } 62 | --- request 63 | GET / 64 | --- no_error_log 65 | [error] 66 | [warn] 67 | --- response_body 68 | OK 69 | 70 | === TEST 2: Mark single host down after 3 fails 71 | --- http_config eval 72 | "$::HttpConfig" 73 | ."$::InitConfig" 74 | . q{ 75 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1 }) 76 | '; 77 | } 78 | --- config 79 | location = / { 80 | content_by_lua ' 81 | -- Simulate 3 connection attempts 82 | for i=1,3 do 83 | upstream:connect() 84 | -- Run process_failed_hosts inline rather than after the request is done 85 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 86 | end 87 | 88 | pools = upstream:get_pools() 89 | 90 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 91 | if pools.primary.hosts[idx].up then 92 | ngx.status = 500 93 | ngx.say("FAIL") 94 | else 95 | ngx.status = 200 96 | ngx.say("OK") 97 | end 98 | ngx.exit(ngx.status) 99 | '; 100 | } 101 | --- request 102 | GET / 103 | --- response_body 104 | OK 105 | 106 | === TEST 3: Mark round_robin host down after 3 fails 107 | --- http_config eval 108 | "$::HttpConfig" 109 | ."$::InitConfig" 110 | . q{ 111 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 9999 }) 112 | test_api:add_host("primary", { id="b", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1 }) 113 | '; 114 | } 115 | --- config 116 | location = / { 117 | content_by_lua ' 118 | 119 | 120 | -- Simulate 3 connection attempts 121 | for i=1,3 do 122 | upstream:connect() 123 | -- Run process_failed_hosts inline rather than after the request is done 124 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 125 | end 126 | 127 | pools = upstream:get_pools() 128 | 129 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 130 | if pools.primary.hosts[idx].up then 131 | ngx.say("FAIL") 132 | ngx.status = 500 133 | else 134 | ngx.say("OK") 135 | ngx.status = 200 136 | end 137 | ngx.exit(ngx.status) 138 | '; 139 | } 140 | --- request 141 | GET / 142 | --- response_body 143 | OK 144 | 145 | === TEST 4: Manually offline hosts are not reset 146 | --- http_config eval 147 | "$::HttpConfig" 148 | ."$::InitConfig" 149 | . q{ 150 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1 }) 151 | '; 152 | } 153 | --- config 154 | location = / { 155 | content_by_lua ' 156 | test_api:down_host("primary", "a") 157 | upstream:revive_hosts() 158 | 159 | local pools, err = upstream:get_pools() 160 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 161 | local host = pools.primary.hosts[idx] 162 | if host.up ~= false then 163 | ngx.status = 500 164 | end 165 | '; 166 | } 167 | --- request 168 | GET / 169 | --- error_code: 200 170 | 171 | === TEST 5: Manually offline hosts are not reset after a natural fail 172 | --- http_config eval 173 | "$::HttpConfig" 174 | ."$::InitConfig" 175 | . q{ 176 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1 }) 177 | '; 178 | } 179 | --- config 180 | location = / { 181 | content_by_lua ' 182 | local pools = upstream:get_pools() 183 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 184 | local host = pools.primary.hosts[idx] 185 | 186 | host.failcount = 1 187 | host.lastfail = ngx.now() - (pools.primary.failed_timeout+1) 188 | upstream:save_pools(pools) 189 | 190 | test_api:down_host("primary", "a") 191 | upstream:revive_hosts() 192 | 193 | local pools, err = upstream:get_pools() 194 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 195 | local host = pools.primary.hosts[idx] 196 | if host.up ~= false then 197 | ngx.status = 500 198 | end 199 | '; 200 | } 201 | --- request 202 | GET / 203 | --- error_code: 200 204 | 205 | === TEST 6: Offline hosts are reset by background function 206 | --- http_config eval 207 | "$::HttpConfig" 208 | ."$::InitConfig" 209 | . q{ 210 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1 }) 211 | '; 212 | } 213 | --- config 214 | location = / { 215 | content_by_lua ' 216 | local pools = upstream:get_pools() 217 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 218 | local host = pools.primary.hosts[idx] 219 | 220 | host.up = false 221 | host.failcount = pools.primary.max_fails +1 222 | host.lastfail = ngx.now() - (pools.primary.failed_timeout+1) 223 | upstream:save_pools(pools) 224 | 225 | upstream:revive_hosts() 226 | 227 | local pools, err = upstream:get_pools() 228 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 229 | local host = pools.primary.hosts[idx] 230 | 231 | if host.up == false or host.failcount ~= 0 or host.lastfail ~= 0 then 232 | ngx.status = 500 233 | end 234 | '; 235 | } 236 | --- request 237 | GET / 238 | --- error_code: 200 239 | 240 | === TEST 7: Do not attempt connection to single host which is down 241 | --- http_config eval 242 | "$::HttpConfig" 243 | ."$::InitConfig" 244 | . q{ 245 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 246 | '; 247 | } 248 | --- log_level: debug 249 | --- config 250 | location = / { 251 | content_by_lua ' 252 | test_api:down_host("primary", "a") 253 | 254 | local ok, err = upstream:connect() 255 | if not ok then 256 | ngx.say("OK") 257 | else 258 | ngx.say(cjson.encode(err)) 259 | end 260 | '; 261 | } 262 | --- request 263 | GET / 264 | --- no_error_log 265 | [error] 266 | [warn] 267 | --- response_body 268 | OK 269 | -------------------------------------------------------------------------------- /t/04-api-pools.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * (29); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | init_by_lua ' 17 | cjson = require "cjson" 18 | upstream_socket = require("resty.upstream.socket") 19 | upstream_api = require("resty.upstream.api") 20 | 21 | upstream, configured = upstream_socket:new("test_upstream") 22 | test_api = upstream_api:new(upstream) 23 | 24 | test_api:create_pool({id = "primary", timeout = 100}) 25 | 26 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10}) 27 | '; 28 | }; 29 | 30 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 31 | 32 | no_long_string(); 33 | #no_diff(); 34 | 35 | run_tests(); 36 | 37 | __DATA__ 38 | === TEST 1: api:get_pools passes through to upstream 39 | --- http_config eval: $::HttpConfig 40 | --- log_level: debug 41 | --- config 42 | location = / { 43 | content_by_lua ' 44 | test_api:add_host("primary", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port, weight = 1 }) 45 | 46 | local pools, err = test_api:get_pools() 47 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 48 | local host = pools.primary.hosts[idx] 49 | if host == nil then 50 | ngx.status = 500 51 | ngx.say(err) 52 | end 53 | '; 54 | } 55 | --- request 56 | GET / 57 | --- error_code: 200 58 | 59 | === TEST 2: create_pool works 60 | --- http_config eval: $::HttpConfig 61 | --- log_level: debug 62 | --- config 63 | location = /a { 64 | content_by_lua ' 65 | local ok,err = test_api:create_pool({id = "test"}) 66 | 67 | local pools, err = upstream:get_pools() 68 | if pools["test"] == nil then 69 | ngx.status = 500 70 | ngx.say(err) 71 | end 72 | ngx.exit(ngx.status) 73 | '; 74 | } 75 | --- request 76 | GET /a 77 | --- errorcode: 200 78 | 79 | === TEST 2b: create_pool with a numeric id creates a string id 80 | --- http_config eval: $::HttpConfig 81 | --- log_level: debug 82 | --- config 83 | location = /a { 84 | content_by_lua ' 85 | local ok, err = test_api:create_pool({id = 1234}) 86 | 87 | local pools, err = upstream:get_pools() 88 | if pools["1234"] == nil or pools[1234] ~= nil then 89 | ngx.status = 500 90 | ngx.say(err) 91 | end 92 | ngx.exit(ngx.status) 93 | '; 94 | } 95 | --- request 96 | GET /a 97 | --- errorcode: 200 98 | 99 | === TEST 3: Cannot add existing pool 100 | --- http_config eval: $::HttpConfig 101 | --- log_level: debug 102 | --- config 103 | location = /a { 104 | content_by_lua ' 105 | local ok,err = test_api:create_pool({id = "primary"}) 106 | if not ok then 107 | ngx.say("OK") 108 | ngx.status = 200 109 | else 110 | ngx.say(err) 111 | ngx.status = 500 112 | end 113 | ngx.exit(ngx.status) 114 | '; 115 | } 116 | --- request 117 | GET /a 118 | --- errorcode: 200 119 | --- response_body 120 | OK 121 | 122 | === TEST 3: Cannot set unavailable load-balancing method 123 | --- http_config eval: $::HttpConfig 124 | --- config 125 | location = /a { 126 | content_by_lua ' 127 | local ok, err = test_api:set_method("primary", "foobar") 128 | if not ok then 129 | ngx.say("OK") 130 | ngx.status = 200 131 | else 132 | ngx.say(err) 133 | ngx.status = 500 134 | end 135 | ngx.exit(ngx.status) 136 | '; 137 | } 138 | --- request 139 | GET /a 140 | --- error_code: 200 141 | --- response_body 142 | OK 143 | 144 | === TEST 3b: Can set available load-balancing method 145 | --- http_config eval: $::HttpConfig 146 | --- config 147 | location = /a { 148 | content_by_lua ' 149 | local ok, err = test_api:set_method("primary", "round_robin") 150 | if ok then 151 | ngx.say("OK") 152 | ngx.status = 200 153 | else 154 | ngx.say(err) 155 | ngx.status = 500 156 | end 157 | ngx.exit(ngx.status) 158 | '; 159 | } 160 | --- request 161 | GET /a 162 | --- error_code: 200 163 | --- response_body 164 | OK 165 | 166 | === TEST 3c: Can set available load-balancing method on numeric id with string arg 167 | --- http_config eval: $::HttpConfig 168 | --- config 169 | location = /a { 170 | content_by_lua ' 171 | local ok, err = test_api:create_pool({id = 1234}) 172 | local ok, err = test_api:set_method("1234", "round_robin") 173 | if ok then 174 | ngx.say("OK") 175 | ngx.status = 200 176 | else 177 | ngx.say(err) 178 | ngx.status = 500 179 | end 180 | ngx.exit(ngx.status) 181 | '; 182 | } 183 | --- request 184 | GET /a 185 | --- error_code: 200 186 | --- response_body 187 | OK 188 | 189 | === TEST 3c: Can set available load-balancing method on numeric id with numeric arg 190 | --- http_config eval: $::HttpConfig 191 | --- config 192 | location = /a { 193 | content_by_lua ' 194 | local ok, err = test_api:create_pool({id = 1234}) 195 | local ok, err = test_api:set_method(1234, "round_robin") 196 | if ok then 197 | ngx.say("OK") 198 | ngx.status = 200 199 | else 200 | ngx.say(err) 201 | ngx.status = 500 202 | end 203 | ngx.exit(ngx.status) 204 | '; 205 | } 206 | --- request 207 | GET /a 208 | --- error_code: 200 209 | --- response_body 210 | OK 211 | 212 | 213 | === TEST 4: Cannot set non-numeric priority 214 | --- http_config eval: $::HttpConfig 215 | --- config 216 | location = /a { 217 | content_by_lua ' 218 | local ok, err = test_api:set_priority("primary", "foobar") 219 | if not ok then 220 | ngx.say("OK") 221 | ngx.status = 200 222 | else 223 | ngx.say(err) 224 | ngx.status = 500 225 | end 226 | ngx.exit(ngx.status) 227 | '; 228 | } 229 | --- request 230 | GET /a 231 | --- error_code: 200 232 | --- response_body 233 | OK 234 | 235 | === TEST 4b: Can set numeric priority 236 | --- http_config eval: $::HttpConfig 237 | --- config 238 | location = /a { 239 | content_by_lua ' 240 | local ok, err = test_api:set_priority("primary", 5) 241 | if ok then 242 | local pools = test_api:get_pools() 243 | if pools.primary.priority ~= 5 then 244 | ngx.say(pools.primary.priority) 245 | ngx.status = 500 246 | else 247 | ngx.say("OK") 248 | ngx.status = 200 249 | end 250 | else 251 | ngx.say(err) 252 | ngx.status = 500 253 | end 254 | ngx.exit(ngx.status) 255 | '; 256 | } 257 | --- request 258 | GET /a 259 | --- error_code: 200 260 | --- response_body 261 | OK 262 | 263 | === TEST 4c: Can set priority on numeric pool with string arg 264 | --- http_config eval: $::HttpConfig 265 | --- config 266 | location = /a { 267 | content_by_lua ' 268 | local ok, err = test_api:create_pool({id = 1234}) 269 | local ok, err = test_api:set_priority("1234", 5) 270 | if ok then 271 | local pools = test_api:get_pools() 272 | if pools["1234"].priority ~= 5 then 273 | ngx.status = 500 274 | else 275 | ngx.say("OK") 276 | ngx.status = 200 277 | end 278 | else 279 | ngx.say(err) 280 | ngx.status = 500 281 | end 282 | ngx.exit(ngx.status) 283 | '; 284 | } 285 | --- request 286 | GET /a 287 | --- error_code: 200 288 | --- response_body 289 | OK 290 | 291 | === TEST 4d: Can set priority on numeric pool with numeric arg 292 | --- http_config eval: $::HttpConfig 293 | --- config 294 | location = /a { 295 | content_by_lua ' 296 | local ok, err = test_api:create_pool({id = 1234}) 297 | local ok, err = test_api:set_priority(1234, 5) 298 | if ok then 299 | local pools = test_api:get_pools() 300 | if pools["1234"].priority ~= 5 then 301 | ngx.status = 500 302 | else 303 | ngx.say("OK") 304 | ngx.status = 200 305 | end 306 | else 307 | ngx.say(err) 308 | ngx.status = 500 309 | end 310 | ngx.exit(ngx.status) 311 | '; 312 | } 313 | --- request 314 | GET /a 315 | --- error_code: 200 316 | --- response_body 317 | OK 318 | 319 | 320 | === TEST 5: Cannot create pool with bad values 321 | --- http_config eval: $::HttpConfig 322 | --- config 323 | location = /a { 324 | content_by_lua ' 325 | local ok, err = test_api:create_pool({ 326 | id = "testpool", 327 | priority = "abcd", 328 | timeout = "foo", 329 | method = "bar", 330 | max_fails = "three", 331 | fail_timeout = "sixty" 332 | }) 333 | 334 | if not ok then 335 | ngx.say("OK") 336 | ngx.status = 200 337 | else 338 | ngx.say(err) 339 | ngx.status = 500 340 | end 341 | ngx.exit(ngx.status) 342 | '; 343 | } 344 | --- request 345 | GET /a 346 | --- error_code: 200 347 | --- response_body 348 | OK 349 | 350 | === TEST 6: Cannot add host to non-existent pool 351 | --- http_config eval: $::HttpConfig 352 | --- config 353 | location = / { 354 | content_by_lua ' 355 | local ok, err = test_api:add_host("foobar", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 356 | if not ok then 357 | ngx.say("OK") 358 | ngx.status = 200 359 | else 360 | ngx.say(err) 361 | ngx.status = 500 362 | end 363 | ngx.exit(ngx.status) 364 | '; 365 | } 366 | --- request 367 | GET / 368 | --- errorcode: 200 369 | --- response_body 370 | OK 371 | 372 | === TEST 6b: Can add host to numeric pool with string arg 373 | --- http_config eval: $::HttpConfig 374 | --- config 375 | location = / { 376 | content_by_lua ' 377 | local ok, err = test_api:create_pool({id = 1234}) 378 | local ok, err = test_api:add_host("1234", { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 379 | 380 | local pools, err = upstream:get_pools() 381 | if #pools["1234"].hosts == 0 then 382 | ngx.status = 500 383 | ngx.say(err) 384 | else 385 | ngx.say("OK") 386 | end 387 | 388 | ngx.exit(ngx.status) 389 | '; 390 | } 391 | --- request 392 | GET / 393 | --- errorcode: 200 394 | --- response_body 395 | OK 396 | 397 | === TEST 6c: Can add host to numeric pool with numeric arg 398 | --- http_config eval: $::HttpConfig 399 | --- config 400 | location = / { 401 | content_by_lua ' 402 | local ok, err = test_api:create_pool({id = 1234}) 403 | local ok, err = test_api:add_host(1234, { id="a", host = ngx.var.server_addr, port = ngx.var.server_port+1, weight = 10 }) 404 | 405 | local pools, err = upstream:get_pools() 406 | if #pools["1234"].hosts == 0 then 407 | --ngx.status = 500 408 | ngx.say(err) 409 | else 410 | ngx.say("OK") 411 | end 412 | 413 | ngx.exit(ngx.status) 414 | '; 415 | } 416 | --- request 417 | GET / 418 | --- errorcode: 200 419 | --- response_body 420 | OK 421 | -------------------------------------------------------------------------------- /t/05-api-hosts.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * (23); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | }; 16 | 17 | our $InitConfig = qq{ 18 | init_by_lua ' 19 | cjson = require "cjson" 20 | upstream_socket = require("resty.upstream.socket") 21 | upstream_api = require("resty.upstream.api") 22 | 23 | upstream, configured = upstream_socket:new("test_upstream") 24 | test_api = upstream_api:new(upstream) 25 | 26 | test_api:create_pool({id = "primary", timeout = 100, status_codes = {["5xx"] = true}}) 27 | 28 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10, status_codes = {["5xx"] = true}}) 29 | }; 30 | 31 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 32 | 33 | no_long_string(); 34 | #no_diff(); 35 | 36 | run_tests(); 37 | 38 | __DATA__ 39 | === TEST 1: add_host works 40 | --- http_config eval 41 | "$::HttpConfig" 42 | ."$::InitConfig" 43 | . q{ 44 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 45 | '; 46 | } 47 | --- log_level: debug 48 | --- config 49 | location = / { 50 | content_by_lua ' 51 | local pools, err = upstream:get_pools() 52 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 53 | if idx == nil then 54 | ngx.status = 500 55 | ngx.say(err) 56 | end 57 | '; 58 | } 59 | --- request 60 | GET / 61 | --- error_code: 200 62 | 63 | === TEST 1b: Cannot add host with existing id 64 | --- http_config eval 65 | "$::HttpConfig" 66 | ."$::InitConfig" 67 | . q{ 68 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 69 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 70 | '; 71 | } 72 | --- log_level: debug 73 | --- config 74 | location = / { 75 | content_by_lua ' 76 | local pools, err = upstream:get_pools() 77 | ngx.say(#pools.primary.hosts) 78 | '; 79 | } 80 | --- request 81 | GET / 82 | --- response_body 83 | 1 84 | 85 | === TEST 1c: add_host with explicit numeric id is converted to string 86 | --- http_config eval 87 | "$::HttpConfig" 88 | ."$::InitConfig" 89 | . q{ 90 | test_api:add_host("primary", { id=123, host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 91 | '; 92 | } 93 | --- log_level: debug 94 | --- config 95 | location = / { 96 | content_by_lua ' 97 | local pools, err = upstream:get_pools() 98 | if not type(pools.primary.hosts[1].id) == "string" then 99 | ngx.status = 500 100 | ngx.say(err) 101 | end 102 | '; 103 | } 104 | --- request 105 | GET / 106 | --- error_code: 200 107 | 108 | === TEST 1d: add_host with implicit numeric id is converted to string 109 | --- http_config eval 110 | "$::HttpConfig" 111 | ."$::InitConfig" 112 | . q{ 113 | test_api:add_host("primary", {host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 114 | '; 115 | } 116 | --- log_level: debug 117 | --- config 118 | location = / { 119 | content_by_lua ' 120 | local pools, err = upstream:get_pools() 121 | if not type(pools.primary.hosts[1].id) == "string" then 122 | ngx.status = 500 123 | ngx.say(err) 124 | end 125 | '; 126 | } 127 | --- request 128 | GET / 129 | --- error_code: 200 130 | 131 | 132 | === TEST 2: Mixed specific and implied host IDs 133 | --- http_config eval 134 | "$::HttpConfig" 135 | ."$::InitConfig" 136 | . q{ 137 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 138 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 139 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 140 | test_api:add_host("primary", { id="foo", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 141 | '; 142 | } 143 | --- config 144 | location = / { 145 | content_by_lua ' 146 | local pools, err = upstream:get_pools() 147 | local ids = {} 148 | for k,v in pairs(pools.primary.hosts) do 149 | table.insert(ids, tostring(v.id)) 150 | end 151 | table.sort(ids) 152 | for k,v in ipairs(ids) do 153 | ngx.say(v) 154 | end 155 | '; 156 | } 157 | --- request 158 | GET / 159 | --- response_body 160 | 1 161 | 3 162 | a 163 | foo 164 | 165 | === TEST 3: down_host marks host down 166 | --- http_config eval 167 | "$::HttpConfig" 168 | ."$::InitConfig" 169 | . q{ 170 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 171 | '; 172 | } 173 | --- config 174 | location = / { 175 | content_by_lua ' 176 | test_api:down_host("primary", "a") 177 | 178 | local pools, err = upstream:get_pools() 179 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 180 | local host = pools.primary.hosts[idx] 181 | if host.up ~= false then 182 | ngx.status = 500 183 | end 184 | '; 185 | } 186 | --- request 187 | GET / 188 | --- error_code: 200 189 | 190 | === TEST 3b: down_host with numeric arg marks host down 191 | --- http_config eval 192 | "$::HttpConfig" 193 | ."$::InitConfig" 194 | . q{ 195 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 196 | '; 197 | } 198 | --- config 199 | location = / { 200 | content_by_lua ' 201 | test_api:down_host("primary", 1) 202 | 203 | local pools, err = upstream:get_pools() 204 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 205 | local host = pools.primary.hosts[idx] 206 | if host.up ~= false then 207 | ngx.status = 500 208 | end 209 | '; 210 | } 211 | --- request 212 | GET / 213 | --- error_code: 200 214 | 215 | 216 | === TEST 3c: down_host with string arg marks host down 217 | --- http_config eval 218 | "$::HttpConfig" 219 | ."$::InitConfig" 220 | . q{ 221 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 222 | '; 223 | } 224 | --- config 225 | location = / { 226 | content_by_lua ' 227 | test_api:down_host("primary", "1") 228 | 229 | local pools, err = upstream:get_pools() 230 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 231 | local host = pools.primary.hosts[idx] 232 | if host.up ~= false then 233 | ngx.status = 500 234 | end 235 | '; 236 | } 237 | --- request 238 | GET / 239 | --- error_code: 200 240 | 241 | === TEST 4: up_host marks host up 242 | --- http_config eval 243 | "$::HttpConfig" 244 | ."$::InitConfig" 245 | . q{ 246 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 247 | '; 248 | } 249 | --- log_level: debug 250 | --- config 251 | location = / { 252 | content_by_lua ' 253 | test_api:down_host("primary", "a") 254 | test_api:up_host("primary", "a") 255 | 256 | local pools, err = upstream:get_pools() 257 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 258 | local host = pools.primary.hosts[idx] 259 | if host.up ~= true then 260 | ngx.status = 500 261 | ngx.say(err) 262 | end 263 | '; 264 | } 265 | --- request 266 | GET / 267 | --- error_code: 200 268 | 269 | === TEST 4b: up_host with numeric arg marks host up 270 | --- http_config eval 271 | "$::HttpConfig" 272 | ."$::InitConfig" 273 | . q{ 274 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 275 | '; 276 | } 277 | --- log_level: debug 278 | --- config 279 | location = / { 280 | content_by_lua ' 281 | test_api:down_host("primary", "1") 282 | test_api:up_host("primary", 1) 283 | 284 | local pools, err = upstream:get_pools() 285 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 286 | local host = pools.primary.hosts[idx] 287 | if host.up ~= true then 288 | ngx.status = 500 289 | ngx.say(err) 290 | end 291 | '; 292 | } 293 | --- request 294 | GET / 295 | --- error_code: 200 296 | 297 | === TEST 4b: up_host with string arg marks host up 298 | --- http_config eval 299 | "$::HttpConfig" 300 | ."$::InitConfig" 301 | . q{ 302 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 303 | '; 304 | } 305 | --- log_level: debug 306 | --- config 307 | location = / { 308 | content_by_lua ' 309 | test_api:down_host("primary", "1") 310 | test_api:up_host("primary", "1") 311 | 312 | local pools, err = upstream:get_pools() 313 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 314 | local host = pools.primary.hosts[idx] 315 | if host.up ~= true then 316 | ngx.status = 500 317 | ngx.say(err) 318 | end 319 | '; 320 | } 321 | --- request 322 | GET / 323 | --- error_code: 200 324 | 325 | 326 | === TEST 5: remove_host deletes host 327 | --- http_config eval 328 | "$::HttpConfig" 329 | ."$::InitConfig" 330 | . q{ 331 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 332 | '; 333 | } 334 | --- log_level: debug 335 | --- config 336 | location = / { 337 | content_by_lua ' 338 | test_api:remove_host("primary", "a") 339 | 340 | local pools, err = upstream:get_pools() 341 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 342 | if idx ~= nil then 343 | ngx.status = 500 344 | ngx.say(err) 345 | end 346 | '; 347 | } 348 | --- request 349 | GET / 350 | --- error_code: 200 351 | 352 | === TEST 5b: remove_host with numeric arg deletes host 353 | --- http_config eval 354 | "$::HttpConfig" 355 | ."$::InitConfig" 356 | . q{ 357 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 358 | '; 359 | } 360 | --- log_level: debug 361 | --- config 362 | location = / { 363 | content_by_lua ' 364 | test_api:remove_host("primary", 1) 365 | 366 | local pools, err = upstream:get_pools() 367 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 368 | if idx ~= nil then 369 | ngx.status = 500 370 | ngx.say(err) 371 | end 372 | '; 373 | } 374 | --- request 375 | GET / 376 | --- error_code: 200 377 | 378 | === TEST 5c: remove_host with string arg deletes host 379 | --- http_config eval 380 | "$::HttpConfig" 381 | ."$::InitConfig" 382 | . q{ 383 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 384 | '; 385 | } 386 | --- log_level: debug 387 | --- config 388 | location = / { 389 | content_by_lua ' 390 | test_api:remove_host("primary", "1") 391 | 392 | local pools, err = upstream:get_pools() 393 | local idx = upstream.get_host_idx("1", pools.primary.hosts) 394 | if idx ~= nil then 395 | ngx.status = 500 396 | ngx.say(err) 397 | end 398 | '; 399 | } 400 | --- request 401 | GET / 402 | --- error_code: 200 403 | 404 | 405 | === TEST 6: Cannot set non-numeric weight 406 | --- http_config eval 407 | "$::HttpConfig" 408 | ."$::InitConfig" 409 | . q{ 410 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 411 | '; 412 | } 413 | --- config 414 | location = /a { 415 | content_by_lua ' 416 | local ok, err = test_api:set_weight("primary", "a", "foobar") 417 | if not ok then 418 | ngx.status = 200 419 | else 420 | ngx.status = 500 421 | end 422 | ngx.exit(ngx.status) 423 | '; 424 | } 425 | --- request 426 | GET /a 427 | --- error_code: 200 428 | 429 | === TEST 7b: Can set numeric weight 430 | --- http_config eval 431 | "$::HttpConfig" 432 | ."$::InitConfig" 433 | . q{ 434 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 435 | '; 436 | } 437 | --- config 438 | location = /a { 439 | content_by_lua ' 440 | local ok, err = test_api:set_weight("primary", "a", 5) 441 | if ok then 442 | local pools = test_api:get_pools() 443 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 444 | local host = pools.primary.hosts[idx] 445 | if host.weight ~= 5 then 446 | ngx.status = 500 447 | ngx.log(ngx.ERR, "Weight set to ".. (pools.primary.hosts.a.weight or "nil")) 448 | else 449 | ngx.status = 200 450 | end 451 | else 452 | ngx.say(err) 453 | ngx.log(ngx.ERR, err) 454 | ngx.status = 500 455 | end 456 | ngx.exit(ngx.status) 457 | '; 458 | } 459 | --- request 460 | GET /a 461 | --- error_code: 200 462 | 463 | === TEST 8: Optional host params can be set 464 | --- http_config eval 465 | "$::HttpConfig" 466 | ."$::InitConfig" 467 | . q{ 468 | local check_params = { 469 | path = "/check", 470 | headers = { 471 | ["User-Agent"] = "Test-Agent" 472 | } 473 | } 474 | test_api:add_host("primary", { 475 | id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, 476 | healthcheck = check_params 477 | }) 478 | '; 479 | } 480 | --- log_level: debug 481 | --- config 482 | location = / { 483 | content_by_lua ' 484 | local check_params = { 485 | path = "/check", 486 | headers = { 487 | ["User-Agent"] = "Test-Agent" 488 | } 489 | } 490 | 491 | local pools, err = upstream:get_pools() 492 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 493 | local host = pools.primary.hosts[idx] 494 | local host_check = host.healthcheck 495 | 496 | local r_compare 497 | r_compare = function (a,b) 498 | for k,v in pairs(a) do 499 | if type(b[k]) == "table" then 500 | if not r_compare(b[k], v) then 501 | return false 502 | end 503 | elseif b[k] ~= v then 504 | return false 505 | end 506 | end 507 | return true 508 | end 509 | 510 | if not r_compare(check_params, host_check) then 511 | ngx.status = 500 512 | end 513 | '; 514 | } 515 | --- request 516 | GET / 517 | --- error_code: 200 518 | 519 | === TEST 9: Cannot set non-numeric host values 520 | --- http_config eval 521 | "$::HttpConfig" 522 | ."$::InitConfig" 523 | . q{ 524 | '; 525 | } 526 | --- config 527 | location = /a { 528 | content_by_lua ' 529 | local ok, err = test_api:add_host("primary", { id="a", host = "127.0.0.1", port = "foo"}) 530 | if not ok then ngx.say(err) end 531 | local ok, err = test_api:add_host("primary", { id="b", host = "127.0.0.1", weight = "foo"}) 532 | if not ok then ngx.say(err) end 533 | local ok, err = test_api:add_host("primary", { id="c", host = "127.0.0.1", failcount = "foo"}) 534 | if not ok then ngx.say(err) end 535 | local ok, err = test_api:add_host("primary", { id="d", host = "127.0.0.1", lastfail = "foo"}) 536 | if not ok then ngx.say(err) end 537 | '; 538 | } 539 | --- request 540 | GET /a 541 | --- error_code: 200 542 | --- response_body 543 | port must be a number 544 | weight must be a number 545 | failcount must be a number 546 | lastfail must be a number 547 | 548 | 549 | === TEST 9: Cannot set numeric string values 550 | --- http_config eval 551 | "$::HttpConfig" 552 | ."$::InitConfig" 553 | . q{ 554 | '; 555 | } 556 | --- config 557 | location = /a { 558 | content_by_lua ' 559 | local ok, err = test_api:add_host("primary", { id="a", host = "127.0.0.1", port = "443"}) 560 | if not ok then ngx.say(err) else ngx.say("OK") end 561 | '; 562 | } 563 | --- request 564 | GET /a 565 | --- error_code: 200 566 | --- response_body 567 | OK 568 | 569 | -------------------------------------------------------------------------------- /t/06-http.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 11; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | }; 16 | 17 | our $InitConfig = qq{ 18 | init_by_lua ' 19 | cjson = require "cjson" 20 | upstream_socket = require("resty.upstream.socket") 21 | upstream_http = require("resty.upstream.http") 22 | upstream_api = require("resty.upstream.api") 23 | 24 | upstream, configured = upstream_socket:new("test_upstream") 25 | test_api = upstream_api:new(upstream) 26 | http = upstream_http:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100, read_timeout = 1100, keepalive_timeout = 1, status_codes = {["5xx"] = true} }) 29 | 30 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10, status_codes = {["5xx"] = true}}) 31 | }; 32 | 33 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 34 | 35 | no_long_string(); 36 | #no_diff(); 37 | 38 | run_tests(); 39 | 40 | __DATA__ 41 | === TEST 1: HTTP Requests pass through 42 | --- http_config eval 43 | "$::HttpConfig" 44 | ."$::InitConfig" 45 | . q{ 46 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 47 | '; 48 | } 49 | --- log_level: debug 50 | --- config 51 | location = /a { 52 | content_by_lua ' 53 | local res, err, status = http:request({ 54 | method = "GET", 55 | path = "/test", 56 | headers = ngx.req.get_headers(), 57 | }) 58 | 59 | if not res then 60 | ngx.status = status 61 | ngx.say(err) 62 | return ngx.exit(ngx.status) 63 | end 64 | 65 | local body = res:read_body() 66 | ngx.print(body) 67 | 68 | '; 69 | } 70 | location = /test { 71 | echo 'response'; 72 | } 73 | --- request 74 | GET /a 75 | --- response_body 76 | response 77 | 78 | === TEST 2: HTTP Status causes failover 79 | --- http_config eval 80 | "$::HttpConfig" 81 | ."$::InitConfig" 82 | . q{ 83 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 84 | test_api:add_host("secondary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 85 | '; 86 | } 87 | --- log_level: debug 88 | --- config 89 | location = /a { 90 | content_by_lua ' 91 | local res, err, status = http:request({ 92 | method = "GET", 93 | path = "/test", 94 | headers = ngx.req.get_headers(), 95 | }) 96 | 97 | if not res then 98 | ngx.status = status 99 | ngx.say(err) 100 | return ngx.exit(ngx.status) 101 | end 102 | 103 | local body = res:read_body() 104 | ngx.print(body) 105 | local host, pool = err.host, err.pool 106 | ngx.say(host.id) 107 | ngx.say(pool.id) 108 | 109 | '; 110 | } 111 | location = /test { 112 | content_by_lua ' 113 | local first = ngx.shared.test_upstream:get("first_flag") 114 | 115 | if not first then 116 | ngx.shared.test_upstream:set("first_flag", true) 117 | ngx.status = 500 118 | ngx.say("error") 119 | return ngx.exit(500) 120 | end 121 | 122 | ngx.say("response") 123 | '; 124 | } 125 | --- request 126 | GET /a 127 | --- response_body 128 | response 129 | b 130 | secondary 131 | 132 | === TEST 3: No connections returns 504 133 | --- http_config eval 134 | "$::HttpConfig" 135 | ."$::InitConfig" 136 | . q{ 137 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 10 }) 138 | test_api:add_host("secondary", { id="b", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 10 }) 139 | '; 140 | } 141 | --- log_level: debug 142 | --- config 143 | location = /a { 144 | content_by_lua ' 145 | local res, err, status = http:request({ 146 | method = "GET", 147 | path = "/test", 148 | headers = ngx.req.get_headers(), 149 | }) 150 | 151 | if not res then 152 | ngx.status = status 153 | ngx.say(err) 154 | return ngx.exit(ngx.status) 155 | end 156 | 157 | local body = res:read_body() 158 | ngx.print(body) 159 | 160 | '; 161 | } 162 | 163 | --- request 164 | GET /a 165 | --- error_code: 504 166 | 167 | === TEST 4: Connection but bad HTTP response returns 502 168 | --- http_config eval 169 | "$::HttpConfig" 170 | ."$::InitConfig" 171 | . q{ 172 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 10 }) 173 | test_api:add_host("secondary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 174 | '; 175 | } 176 | --- log_level: debug 177 | --- config 178 | location = /a { 179 | content_by_lua ' 180 | local res, err, status = http:request({ 181 | method = "GET", 182 | path = "/test", 183 | headers = ngx.req.get_headers(), 184 | }) 185 | 186 | if not res then 187 | ngx.status = status 188 | ngx.say(err) 189 | return ngx.exit(ngx.status) 190 | end 191 | 192 | local body = res:read_body() 193 | ngx.print(body) 194 | 195 | '; 196 | } 197 | location = /test { 198 | content_by_lua ' 199 | ngx.status = 500 200 | ngx.say("error") 201 | return ngx.exit(500) 202 | '; 203 | } 204 | 205 | --- request 206 | GET /a 207 | --- error_code: 502 208 | 209 | === TEST 5: Read timeout can be set 210 | --- http_config eval 211 | "$::HttpConfig" 212 | ."$::InitConfig" 213 | . q{ 214 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 215 | '; 216 | } 217 | --- log_level: debug 218 | --- config 219 | location = /a { 220 | content_by_lua ' 221 | local res, err, status = http:request({ 222 | method = "GET", 223 | path = "/test", 224 | headers = ngx.req.get_headers(), 225 | }) 226 | 227 | if not res then 228 | ngx.status = status 229 | ngx.say(err) 230 | return ngx.exit(ngx.status) 231 | end 232 | 233 | local body = res:read_body() 234 | ngx.print(body) 235 | 236 | '; 237 | } 238 | location = /test { 239 | content_by_lua ' 240 | ngx.sleep(1) 241 | ngx.say("slow!") 242 | '; 243 | } 244 | 245 | --- request 246 | GET /a 247 | --- error_code: 200 248 | 249 | === TEST 6: Pool keepalive overrides set_keepalive call 250 | --- http_config eval 251 | "$::HttpConfig" 252 | ."$::InitConfig" 253 | . q{ 254 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 255 | '; 256 | } 257 | --- log_level: debug 258 | --- config 259 | location = /a { 260 | content_by_lua ' 261 | local res, err, status = http:request({ 262 | method = "GET", 263 | path = "/test", 264 | --headers = ngx.req.get_headers(), 265 | }) 266 | 267 | if not res then 268 | ngx.status = status 269 | ngx.say(err) 270 | return ngx.exit(ngx.status) 271 | end 272 | 273 | local body = res:read_body() 274 | 275 | 276 | local ok, err = http:set_keepalive(100,100) 277 | if not ok then 278 | ngx.say(err) 279 | end 280 | 281 | ngx.sleep(0.1) 282 | 283 | local res, err = http:request({ 284 | method = "GET", 285 | path = "/test", 286 | headers = ngx.req.get_headers(), 287 | }) 288 | 289 | local reuse = http:get_reused_times() 290 | ngx.say(reuse) 291 | 292 | '; 293 | } 294 | location = /test { 295 | content_by_lua ' 296 | ngx.say("ok") 297 | '; 298 | } 299 | 300 | --- request 301 | GET /a 302 | --- response_body 303 | 0 304 | 305 | === TEST 7: Do not retry with streamed request body 306 | --- http_config eval 307 | "$::HttpConfig" 308 | ."$::InitConfig" 309 | . q{ 310 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 311 | test_api:add_host("secondary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 312 | '; 313 | } 314 | --- log_level: debug 315 | --- config 316 | location = /a { 317 | content_by_lua ' 318 | local client_body_reader, err = http:get_client_body_reader() 319 | local res, err, status = http:request({ 320 | method = ngx.req.get_method(), 321 | path = "/test", 322 | body = client_body_reader, 323 | headers = ngx.req.get_headers(), 324 | }) 325 | 326 | if not res then 327 | ngx.status = status 328 | ngx.say(err) 329 | return ngx.exit(status) 330 | end 331 | 332 | ngx.say("Fail") 333 | 334 | '; 335 | } 336 | location = /test { 337 | content_by_lua ' 338 | local first = ngx.shared.test_upstream:get("first_flag") 339 | 340 | if not first then 341 | ngx.shared.test_upstream:set("first_flag", true) 342 | ngx.status = 500 343 | return ngx.exit(500) 344 | end 345 | 346 | ngx.say("response") 347 | '; 348 | } 349 | --- request 350 | POST /a 351 | Hello World 352 | --- error_code: 502 353 | --- response_body 354 | 500 355 | -------------------------------------------------------------------------------- /t/07-locking.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * (16); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | init_by_lua ' 17 | cjson = require "cjson" 18 | upstream_socket = require("resty.upstream.socket") 19 | upstream_api = require("resty.upstream.api") 20 | 21 | upstream, configured = upstream_socket:new("test_upstream") 22 | test_api = upstream_api:new(upstream) 23 | 24 | test_api:create_pool({id = "primary", timeout = 100}) 25 | test_api:set_priority("primary", 5) 26 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 80, weight = 1 }) 27 | '; 28 | }; 29 | 30 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 31 | 32 | no_long_string(); 33 | #no_diff(); 34 | 35 | run_tests(); 36 | 37 | __DATA__ 38 | === TEST 1: Cannot add pool while pools locked 39 | --- http_config eval: $::HttpConfig 40 | --- config 41 | location = /a { 42 | content_by_lua ' 43 | local pools, err = test_api:get_locked_pools() 44 | 45 | local ok, err = test_api:create_pool({id = "secondary", timeout = 100, priority = 10}) 46 | ngx.say(err) 47 | 48 | local ok, err = test_api:unlock_pools() 49 | local pools = cjson.decode(upstream.dict:get(upstream.pools_key)) 50 | for k,v in pairs(pools) do 51 | ngx.say(k) 52 | end 53 | '; 54 | } 55 | --- request 56 | GET /a 57 | --- response_body 58 | locked 59 | primary 60 | 61 | 62 | === TEST 2: Cannot set pool priority while pools locked 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | location = /a { 66 | content_by_lua ' 67 | local pools, err = test_api:get_locked_pools() 68 | 69 | local ok, err = test_api:set_priority("secondary", 11) 70 | ngx.say(err) 71 | 72 | local ok, err = test_api:unlock_pools() 73 | 74 | local pools = cjson.decode(upstream.dict:get(upstream.pools_key)) 75 | ngx.say(pools.primary.priority) 76 | 77 | '; 78 | } 79 | --- request 80 | GET /a 81 | --- response_body 82 | locked 83 | 5 84 | 85 | 86 | === TEST 3: Cannot add host while pools locked 87 | --- http_config eval: $::HttpConfig 88 | --- config 89 | location = /a { 90 | content_by_lua ' 91 | local pools, err = test_api:get_locked_pools() 92 | 93 | local ok, err = test_api:add_host("primary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 94 | ngx.say(err) 95 | 96 | local ok, err = test_api:unlock_pools() 97 | local pools = cjson.decode(upstream.dict:get(upstream.pools_key)) 98 | for k,v in pairs(pools.primary.hosts) do 99 | ngx.say(pools.primary.hosts[k].id) 100 | end 101 | '; 102 | } 103 | --- request 104 | GET /a 105 | --- response_body 106 | locked 107 | a 108 | 109 | 110 | === TEST 4: Cannot remove host while pools locked 111 | --- http_config eval: $::HttpConfig 112 | --- config 113 | location = /a { 114 | content_by_lua ' 115 | local pools, err = test_api:get_locked_pools() 116 | 117 | local ok, err = test_api:remove_host("primary", "a") 118 | ngx.say(err) 119 | 120 | local ok, err = test_api:unlock_pools() 121 | local pools = cjson.decode(upstream.dict:get(upstream.pools_key)) 122 | for k,v in pairs(pools.primary.hosts) do 123 | ngx.say(pools.primary.hosts[k].id) 124 | end 125 | '; 126 | } 127 | --- request 128 | GET /a 129 | --- response_body 130 | locked 131 | a 132 | 133 | === TEST 5: Cannot set host weight while pools locked 134 | --- http_config eval: $::HttpConfig 135 | --- config 136 | location = /a { 137 | content_by_lua ' 138 | local pools, err = test_api:get_locked_pools() 139 | 140 | local ok, err = test_api:set_weight("primary", "a", 11) 141 | ngx.say(err) 142 | 143 | local ok, err = test_api:unlock_pools() 144 | 145 | local pools = cjson.decode(upstream.dict:get(upstream.pools_key)) 146 | ngx.say(pools.primary.hosts[upstream.get_host_idx("a", pools.primary.hosts)].weight) 147 | '; 148 | } 149 | --- request 150 | GET /a 151 | --- response_body 152 | locked 153 | 1 154 | 155 | 156 | === TEST 6: Cannot set host down while pools locked 157 | --- http_config eval: $::HttpConfig 158 | --- config 159 | location = /a { 160 | content_by_lua ' 161 | local pools, err = test_api:get_locked_pools() 162 | 163 | local ok, err = test_api:down_host("primary", "a") 164 | ngx.say(err) 165 | 166 | local ok, err = test_api:unlock_pools() 167 | ngx.say(pools.primary.hosts[upstream.get_host_idx("a", pools.primary.hosts)].up) 168 | '; 169 | } 170 | --- request 171 | GET /a 172 | --- response_body 173 | locked 174 | true 175 | 176 | 177 | === TEST 7: Cannot set host up while pools locked 178 | --- http_config eval: $::HttpConfig 179 | --- config 180 | location = /a { 181 | content_by_lua ' 182 | test_api:down_host("primary", "a") 183 | local pools, err = test_api:get_locked_pools() 184 | 185 | local ok, err = test_api:up_host("primary", "a") 186 | ngx.say(err) 187 | 188 | local ok, err = test_api:unlock_pools() 189 | ngx.say(pools.primary.hosts[upstream.get_host_idx("a", pools.primary.hosts)].up) 190 | '; 191 | } 192 | --- request 193 | GET /a 194 | --- response_body 195 | locked 196 | false 197 | 198 | 199 | === TEST 8: revive_hosts returns nil when locked 200 | --- http_config eval: $::HttpConfig 201 | --- config 202 | location = /a { 203 | content_by_lua ' 204 | local pools, err = upstream:get_locked_pools() 205 | 206 | local ok, err = upstream:revive_hosts() 207 | if not ok then 208 | ngx.say(err) 209 | else 210 | ngx.say("wat") 211 | end 212 | 213 | local ok, err = upstream:unlock_pools() 214 | '; 215 | } 216 | --- request 217 | GET /a 218 | --- response_body 219 | locked 220 | -------------------------------------------------------------------------------- /t/08-round_robin.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 32; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | 17 | }; 18 | 19 | our $InitConfig = qq{ 20 | init_by_lua ' 21 | cjson = require "cjson" 22 | upstream_socket = require("resty.upstream.socket") 23 | upstream_api = require("resty.upstream.api") 24 | 25 | upstream, configured = upstream_socket:new("test_upstream") 26 | test_api = upstream_api:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100}) 29 | test_api:set_method("primary", "round_robin") 30 | 31 | }; 32 | 33 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 34 | 35 | no_long_string(); 36 | #no_diff(); 37 | 38 | run_tests(); 39 | 40 | __DATA__ 41 | 42 | === TEST 1: Round robin method, single host 43 | --- http_config eval 44 | "$::HttpConfig" 45 | ."$::InitConfig" 46 | . q{ 47 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 48 | '; 49 | } 50 | --- log_level: debug 51 | --- config 52 | location = / { 53 | content_by_lua ' 54 | local sock, info = upstream:connect() 55 | if not sock then 56 | ngx.log(ngx.ERR, info) 57 | else 58 | ngx.say(info.host.id) 59 | sock:setkeepalive() 60 | end 61 | 62 | upstream:process_failed_hosts() 63 | '; 64 | } 65 | --- request 66 | GET / 67 | --- no_error_log 68 | [error] 69 | [warn] 70 | --- response_body 71 | 1 72 | 73 | === TEST 2: Round robin between multiple hosts, default settings 74 | --- http_config eval 75 | "$::HttpConfig" 76 | ."$::InitConfig" 77 | . q{ 78 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 79 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 80 | '; 81 | } 82 | --- log_level: debug 83 | --- config 84 | location = / { 85 | content_by_lua ' 86 | local sock, info = upstream:connect() 87 | if not sock then 88 | ngx.log(ngx.ERR, info) 89 | else 90 | ngx.say(info.host.id) 91 | sock:setkeepalive() 92 | end 93 | 94 | 95 | upstream:process_failed_hosts() 96 | '; 97 | } 98 | --- request 99 | GET / 100 | --- no_error_log 101 | [error] 102 | [warn] 103 | --- response_body 104 | 1 105 | 106 | === TEST 3: Round robin is consistent 107 | --- http_config eval 108 | "$::HttpConfig" 109 | ."$::InitConfig" 110 | . q{ 111 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 112 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 113 | '; 114 | } 115 | --- log_level: debug 116 | --- config 117 | location = / { 118 | content_by_lua ' 119 | 120 | local count = 10 121 | for i=1,count do 122 | local sock, info = upstream:connect() 123 | if not sock then 124 | ngx.log(ngx.ERR, info) 125 | else 126 | ngx.print(info.host.id) 127 | sock:setkeepalive() 128 | end 129 | end 130 | 131 | upstream:process_failed_hosts() 132 | '; 133 | } 134 | --- request 135 | GET / 136 | --- no_error_log 137 | [error] 138 | [warn] 139 | --- response_body: 1212121212 140 | 141 | === TEST 4: Round robin with user provided weights 142 | --- http_config eval 143 | "$::HttpConfig" 144 | ."$::InitConfig" 145 | . q{ 146 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 147 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 148 | '; 149 | } 150 | --- log_level: debug 151 | --- config 152 | location = / { 153 | content_by_lua ' 154 | 155 | local count = 10 156 | for i=1,count do 157 | local sock, info = upstream:connect() 158 | if not sock then 159 | ngx.log(ngx.ERR, info) 160 | else 161 | ngx.print(info.host.id) 162 | sock:setkeepalive() 163 | end 164 | end 165 | 166 | upstream:process_failed_hosts() 167 | '; 168 | } 169 | --- request 170 | GET / 171 | --- no_error_log 172 | [error] 173 | [warn] 174 | --- response_body: 1212121212 175 | 176 | === TEST 5: Weighted round robin is consistent 177 | --- http_config eval 178 | "$::HttpConfig" 179 | ."$::InitConfig" 180 | . q{ 181 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 2 }) 182 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 183 | '; 184 | } 185 | --- log_level: debug 186 | --- config 187 | location = / { 188 | content_by_lua ' 189 | 190 | local count = 10 191 | for i=1,count do 192 | local sock, info = upstream:connect() 193 | if not sock then 194 | ngx.log(ngx.ERR, info) 195 | else 196 | ngx.print(info.host.id) 197 | sock:setkeepalive() 198 | end 199 | end 200 | 201 | upstream:process_failed_hosts() 202 | '; 203 | } 204 | --- request 205 | GET / 206 | --- no_error_log 207 | [error] 208 | [warn] 209 | --- response_body: 1121121121 210 | 211 | === TEST 5b: Weighted round robin is consistent, odd number of hosts 212 | --- http_config eval 213 | "$::HttpConfig" 214 | ."$::InitConfig" 215 | . q{ 216 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 217 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 20 }) 218 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 30 }) 219 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 40 }) 220 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 50 }) 221 | '; 222 | } 223 | --- log_level: debug 224 | --- config 225 | location = / { 226 | content_by_lua ' 227 | 228 | local count = 20 229 | for i=1,count do 230 | local sock, info = upstream:connect() 231 | if not sock then 232 | ngx.log(ngx.ERR, info) 233 | else 234 | ngx.print(info.host.id) 235 | sock:setkeepalive() 236 | end 237 | end 238 | 239 | upstream:process_failed_hosts() 240 | '; 241 | } 242 | --- request 243 | GET / 244 | --- no_error_log 245 | [error] 246 | [warn] 247 | --- response_body: 45345234512345453452 248 | 249 | === TEST 5c: Weighted round robin is consistent, last host has highest weight 250 | --- http_config eval 251 | "$::HttpConfig" 252 | ."$::InitConfig" 253 | . q{ 254 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 255 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 2 }) 256 | '; 257 | } 258 | --- log_level: debug 259 | --- config 260 | location = / { 261 | content_by_lua ' 262 | 263 | local count = 10 264 | for i=1,count do 265 | local sock, info = upstream:connect() 266 | if not sock then 267 | ngx.log(ngx.ERR, info) 268 | else 269 | ngx.print(info.host.id) 270 | sock:setkeepalive() 271 | end 272 | end 273 | 274 | upstream:process_failed_hosts() 275 | '; 276 | } 277 | --- request 278 | GET / 279 | --- no_error_log 280 | [error] 281 | [warn] 282 | --- response_body: 2122122122 283 | 284 | === TEST 6: Weighted round robin is consistent, divisable weights 285 | --- http_config eval 286 | "$::HttpConfig" 287 | ."$::InitConfig" 288 | . q{ 289 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 20 }) 290 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 291 | '; 292 | } 293 | --- log_level: debug 294 | --- config 295 | location = / { 296 | content_by_lua ' 297 | 298 | local count = 10 299 | for i=1,count do 300 | local sock, info = upstream:connect() 301 | if not sock then 302 | ngx.log(ngx.ERR, info) 303 | else 304 | ngx.print(info.host.id) 305 | sock:setkeepalive() 306 | end 307 | end 308 | 309 | upstream:process_failed_hosts() 310 | '; 311 | } 312 | --- request 313 | GET / 314 | --- no_error_log 315 | [error] 316 | [warn] 317 | --- response_body: 1121121121 318 | -------------------------------------------------------------------------------- /t/09-events.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => 17; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | 17 | }; 18 | 19 | our $InitConfig = qq{ 20 | init_by_lua ' 21 | cjson = require "cjson" 22 | upstream_socket = require("resty.upstream.socket") 23 | upstream_api = require("resty.upstream.api") 24 | 25 | upstream, configured = upstream_socket:new("test_upstream") 26 | test_api = upstream_api:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100}) 29 | 30 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10}) 31 | }; 32 | 33 | 34 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 35 | 36 | no_long_string(); 37 | #no_diff(); 38 | 39 | run_tests(); 40 | 41 | __DATA__ 42 | === TEST 1: Can bind to an event 43 | --- http_config eval 44 | "$::HttpConfig" 45 | ."$::InitConfig" 46 | . q{ 47 | '; 48 | } 49 | --- log_level: debug 50 | --- config 51 | location = /a { 52 | content_by_lua ' 53 | 54 | local ok, err = upstream:bind("host_up", function(event) 55 | end) 56 | if not ok then 57 | ngx.say(err) 58 | else 59 | ngx.say("OK") 60 | end 61 | '; 62 | } 63 | --- request 64 | GET /a 65 | --- response_body 66 | OK 67 | 68 | === TEST 1b: Cannot bind to a non-existent event 69 | --- http_config eval 70 | "$::HttpConfig" 71 | ."$::InitConfig" 72 | . q{ 73 | '; 74 | } 75 | --- log_level: debug 76 | --- config 77 | location = /a { 78 | content_by_lua ' 79 | 80 | local ok, err = upstream:bind("foobar", function(event) 81 | end) 82 | if not ok then 83 | ngx.say(err) 84 | end 85 | '; 86 | } 87 | --- request 88 | GET /a 89 | --- response_body 90 | Event not found 91 | 92 | === TEST 2: Can't bind a string 93 | --- http_config eval 94 | "$::HttpConfig" 95 | ."$::InitConfig" 96 | . q{ 97 | '; 98 | } 99 | --- log_level: debug 100 | --- config 101 | location = /a { 102 | content_by_lua ' 103 | 104 | local ok, err = upstream:bind("host_down", "foobar") 105 | if not ok then 106 | ngx.say(err) 107 | end 108 | '; 109 | } 110 | --- request 111 | GET /a 112 | --- response_body 113 | Can only bind a function 114 | 115 | === TEST 2b: Can't bind a table 116 | --- http_config eval 117 | "$::HttpConfig" 118 | ."$::InitConfig" 119 | . q{ 120 | '; 121 | } 122 | --- log_level: debug 123 | --- config 124 | location = /a { 125 | content_by_lua ' 126 | 127 | local ok, err = upstream:bind("host_down", {}) 128 | if not ok then 129 | ngx.say(err) 130 | end 131 | '; 132 | } 133 | --- request 134 | GET /a 135 | --- response_body 136 | Can only bind a function 137 | 138 | === TEST 3: bind passes through http upstream 139 | --- http_config eval 140 | "$::HttpConfig" 141 | ."$::InitConfig" 142 | . q{ 143 | '; 144 | } 145 | --- log_level: debug 146 | --- config 147 | location = /a { 148 | content_by_lua ' 149 | local upstream_http = require("resty.upstream.http") 150 | http = upstream_http:new(upstream) 151 | 152 | local ok, err = http:bind("host_down", function(e) end ) 153 | if not ok then 154 | ngx.say(err) 155 | else 156 | ngx.say("OK") 157 | end 158 | '; 159 | } 160 | --- request 161 | GET /a 162 | --- response_body 163 | OK 164 | 165 | === TEST 4: host_down event fires 166 | --- http_config eval 167 | "$::HttpConfig" 168 | ."$::InitConfig" 169 | . q{ 170 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1, max_fails = 1 }) 171 | '; 172 | } 173 | --- config 174 | location = / { 175 | content_by_lua ' 176 | -- Bind event 177 | local function host_down_handler(event) 178 | ngx.say("host_down fired!") 179 | ngx.say("host_id = ", event.host.id) 180 | ngx.say("host = ", event.host.host) 181 | ngx.say("host_up = ", event.host.up) 182 | ngx.say("pool = ", event.pool.id) 183 | end 184 | local ok, err = upstream:bind("host_down", host_down_handler) 185 | if not ok then 186 | ngx.say(err) 187 | end 188 | 189 | -- Simulate 2 connection attempts 190 | for i=1,3 do 191 | upstream:connect() 192 | -- Run process_failed_hosts inline rather than after the request is done 193 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 194 | end 195 | 196 | '; 197 | } 198 | --- request 199 | GET / 200 | --- response_body 201 | host_down fired! 202 | host_id = a 203 | host = 127.0.0.1 204 | host_up = false 205 | pool = primary 206 | 207 | 208 | === TEST 5: host_up event fires 209 | --- http_config eval 210 | "$::HttpConfig" 211 | ."$::InitConfig" 212 | . q{ 213 | test_api:add_host("primary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, max_fails = 1, up = false, lastfail = 1 }) 214 | '; 215 | } 216 | --- config 217 | location = / { 218 | content_by_lua ' 219 | -- Bind event 220 | local function host_up_handler(event) 221 | ngx.say("host_up fired!") 222 | ngx.say("host_id = ", event.host.id) 223 | ngx.say("host = ", event.host.host) 224 | ngx.say("host_up = ", event.host.up) 225 | ngx.say("pool = ", event.pool.id) 226 | end 227 | local ok, err = upstream:bind("host_up", host_up_handler) 228 | if not ok then 229 | ngx.say(err) 230 | end 231 | 232 | -- Run background func inline rather than after the request is done 233 | upstream:revive_hosts() 234 | 235 | '; 236 | } 237 | --- request 238 | GET / 239 | --- no_error_log: error 240 | --- response_body 241 | host_up fired! 242 | host_id = b 243 | host = 127.0.0.1 244 | host_up = true 245 | pool = primary 246 | 247 | === TEST 6: host_up event does not fire when reseting failcount 248 | --- http_config eval 249 | "$::HttpConfig" 250 | ."$::InitConfig" 251 | . q{ 252 | test_api:add_host("primary", { id="b", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, failcount = 1, max_fails = 2, lastfail = 1 }) 253 | '; 254 | } 255 | --- config 256 | location = / { 257 | content_by_lua ' 258 | -- Bind event 259 | local function host_up_handler(event) 260 | ngx.say("host_up fired!") 261 | local cjson = require("cjson") 262 | local log = { 263 | host_id = event.host.id, 264 | host = event.host.host, 265 | host_max_fails = event.host.max_fails, 266 | host_up = event.host.up, 267 | pool = event.pool.id 268 | } 269 | ngx.say(cjson.encode(log)) 270 | end 271 | local ok, err = upstream:bind("host_up", host_up_handler) 272 | if not ok then 273 | ngx.say(err) 274 | end 275 | 276 | -- Run background func inline rather than after the request is done 277 | upstream:revive_hosts() 278 | 279 | '; 280 | } 281 | --- request 282 | GET / 283 | --- response_body_unlike 284 | host_up fired! 285 | -------------------------------------------------------------------------------- /t/10-http-healthcheck.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 39; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | }; 16 | 17 | our $InitConfig = qq{ 18 | init_by_lua ' 19 | cjson = require "cjson" 20 | upstream_socket = require("resty.upstream.socket") 21 | upstream_http = require("resty.upstream.http") 22 | upstream_api = require("resty.upstream.api") 23 | 24 | upstream, configured = upstream_socket:new("test_upstream") 25 | test_api = upstream_api:new(upstream) 26 | http = upstream_http:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100, read_timeout = 1100, keepalive_timeout = 1, status_codes = {["5xx"] = true} }) 29 | 30 | test_api:create_pool({id = "secondary", timeout = 100, priority = 10, status_codes = {["5xx"] = true}}) 31 | }; 32 | 33 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 34 | 35 | no_long_string(); 36 | #no_diff(); 37 | 38 | run_tests(); 39 | 40 | __DATA__ 41 | === TEST 1: Default background check is sent - true 42 | --- http_config eval 43 | "$::HttpConfig" 44 | ."$::InitConfig" 45 | . q{ 46 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = true }) 47 | '; 48 | } 49 | --- config 50 | location = / { 51 | content_by_lua ' 52 | ngx.log(ngx.ERR, "Background check received") 53 | '; 54 | } 55 | location = /foo { 56 | content_by_lua ' 57 | http:_http_background_func() 58 | '; 59 | } 60 | --- request 61 | GET /foo 62 | --- error_log: Background check received 63 | 64 | === TEST 1b: Default background check is sent - table 65 | --- http_config eval 66 | "$::HttpConfig" 67 | ."$::InitConfig" 68 | . q{ 69 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = {} }) 70 | '; 71 | } 72 | --- config 73 | location = / { 74 | content_by_lua ' 75 | ngx.log(ngx.ERR, "Background check received") 76 | '; 77 | } 78 | location = /foo { 79 | content_by_lua ' 80 | http:_http_background_func() 81 | '; 82 | } 83 | --- request 84 | GET /foo 85 | --- error_log: Background check received 86 | 87 | === TEST 1c: Default background check can be disabled - nil 88 | --- http_config eval 89 | "$::HttpConfig" 90 | ."$::InitConfig" 91 | . q{ 92 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = nil }) 93 | '; 94 | } 95 | --- config 96 | location = / { 97 | content_by_lua ' 98 | ngx.log(ngx.ERR, "Background check received") 99 | '; 100 | } 101 | location = /foo { 102 | content_by_lua ' 103 | http:_http_background_func() -- Should not fire background check 104 | ngx.log(ngx.ERR, "Second log entry") 105 | '; 106 | } 107 | --- request 108 | GET /foo 109 | --- no_error_log: Background check received 110 | --- error_log: Second log entry 111 | 112 | === TEST 1d: Default background check can be disabled - false 113 | --- http_config eval 114 | "$::HttpConfig" 115 | ."$::InitConfig" 116 | . q{ 117 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = false }) 118 | '; 119 | } 120 | --- config 121 | location = / { 122 | content_by_lua ' 123 | ngx.log(ngx.ERR, "Background check received") 124 | '; 125 | } 126 | location = /foo { 127 | content_by_lua ' 128 | http:_http_background_func() -- Should not fire background check 129 | ngx.log(ngx.ERR, "Second log entry") 130 | '; 131 | } 132 | --- request 133 | GET /foo 134 | --- no_error_log: Background check received 135 | --- error_log: Second log entry 136 | 137 | === TEST 2: Custom background check params 138 | --- http_config eval 139 | "$::HttpConfig" 140 | ."$::InitConfig" 141 | . q{ 142 | test_api:add_host("primary", { 143 | id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, 144 | healthcheck = { 145 | path = "/check", 146 | headers = { 147 | ["User-Agent"] = "Test-Agent" 148 | } 149 | } 150 | }) 151 | '; 152 | } 153 | --- config 154 | location = /check { 155 | content_by_lua ' 156 | local headers = ngx.req.get_headers() 157 | ngx.log(ngx.ERR, "Background check received from "..headers["User-Agent"]) 158 | '; 159 | } 160 | location = /foo { 161 | content_by_lua ' 162 | http:_http_background_func() 163 | '; 164 | } 165 | --- request 166 | GET /foo 167 | --- error_log: Background check received from Test-Agent 168 | 169 | === TEST 3: Background check marks timeout host failed 170 | --- http_config eval 171 | "$::HttpConfig" 172 | ."$::InitConfig" 173 | . q{ 174 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = 8$TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = true }) 175 | '; 176 | } 177 | --- config 178 | location = / { 179 | content_by_lua ' 180 | http:_http_background_func() 181 | -- Run process_failed_hosts inline rather than after the request is done 182 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 183 | 184 | local pools, err = upstream:get_pools() 185 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 186 | local host = pools.primary.hosts[idx] 187 | if host.failcount ~= 1 then 188 | ngx.status = 500 189 | end 190 | 191 | '; 192 | } 193 | --- request 194 | GET / 195 | --- error_code: 200 196 | 197 | === TEST 4: Background check marks http error host failed 198 | --- http_config eval 199 | "$::HttpConfig" 200 | ."$::InitConfig" 201 | . q{ 202 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = true }) 203 | '; 204 | } 205 | --- config 206 | location = / { 207 | return 500; 208 | } 209 | location = /foo { 210 | content_by_lua ' 211 | http:_http_background_func() 212 | -- Run process_failed_hosts inline rather than after the request is done 213 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 214 | 215 | local pools, err = upstream:get_pools() 216 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 217 | local host = pools.primary.hosts[idx] 218 | if host.failcount ~= 1 then 219 | ngx.status = 500 220 | end 221 | 222 | '; 223 | } 224 | --- request 225 | GET /foo 226 | --- error_code: 200 227 | 228 | === TEST 5: Succesful healthcheck request doesn't affect host 229 | --- http_config eval 230 | "$::HttpConfig" 231 | ."$::InitConfig" 232 | . q{ 233 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = true }) 234 | '; 235 | } 236 | --- config 237 | location = / { 238 | return 200; 239 | } 240 | location = /foo { 241 | content_by_lua ' 242 | http:_http_background_func() 243 | -- Run process_failed_hosts inline rather than after the request is done 244 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 245 | 246 | local pools, err = upstream:get_pools() 247 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 248 | local host = pools.primary.hosts[idx] 249 | if host.failcount ~= 0 then 250 | ngx.status = 500 251 | end 252 | 253 | '; 254 | } 255 | --- request 256 | GET /foo 257 | --- error_code: 200 258 | 259 | === TEST 6: Custom healthcheck interval 260 | --- http_config eval 261 | "$::HttpConfig" 262 | ."$::InitConfig" 263 | . q{ 264 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = {interval = 2} }) 265 | '; 266 | } 267 | --- config 268 | location = / { 269 | content_by_lua ' 270 | local first = ngx.shared.test_upstream:get("first_flag") 271 | 272 | if not first then 273 | ngx.shared.test_upstream:set("first_flag", true) 274 | ngx.log(ngx.ERR, "Background check received") 275 | else 276 | ngx.log(ngx.ERR, "Second Background check received") 277 | end 278 | '; 279 | } 280 | location = /foo { 281 | content_by_lua ' 282 | http:_http_background_func() 283 | ngx.sleep(2) 284 | http:_http_background_func() 285 | '; 286 | } 287 | --- request 288 | GET /foo 289 | --- error_log: Background check received 290 | --- error_log: Second Background check received 291 | 292 | === TEST 6: Required healthcheck values get set 293 | --- http_config eval 294 | "$::HttpConfig" 295 | ."$::InitConfig" 296 | . q{ 297 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = true }) 298 | '; 299 | } 300 | --- config 301 | location = / { 302 | content_by_lua ' 303 | ngx.shared.test_upstream:set("first_flag", true) 304 | ngx.log(ngx.ERR, "Background check received") 305 | '; 306 | } 307 | location = /foo { 308 | content_by_lua ' 309 | http:_http_background_func() 310 | 311 | local pools, err = upstream:get_pools() 312 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 313 | local host = pools.primary.hosts[idx] 314 | ngx.print(host.healthcheck.interval) 315 | if host.healthcheck.last_check then 316 | ngx.print(" last_check") 317 | end 318 | '; 319 | } 320 | --- request 321 | GET /foo 322 | --- error_log: Background check received 323 | --- response_body: 60 last_check 324 | 325 | === TEST 7: Default background check params are set - bool 326 | --- http_config eval 327 | "$::HttpConfig" 328 | ."$::InitConfig" 329 | . q{ 330 | test_api:add_host("primary", { 331 | id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, 332 | healthcheck = true 333 | }) 334 | '; 335 | } 336 | --- config 337 | location = / { 338 | content_by_lua ' 339 | local headers = ngx.req.get_headers() 340 | ngx.log(ngx.ERR, "Background check received from "..headers["User-Agent"]) 341 | '; 342 | } 343 | location = /foo { 344 | content_by_lua ' 345 | http:_http_background_func() 346 | '; 347 | } 348 | --- request 349 | GET /foo 350 | --- error_log: Background check received from Resty Upstream 351 | 352 | === TEST 7b: Default background check params are set - table 353 | --- http_config eval 354 | "$::HttpConfig" 355 | ."$::InitConfig" 356 | . q{ 357 | test_api:add_host("primary", { 358 | id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, 359 | healthcheck = { path = "/check" } 360 | }) 361 | '; 362 | } 363 | --- config 364 | location = /check { 365 | content_by_lua ' 366 | local headers = ngx.req.get_headers() 367 | ngx.log(ngx.ERR, "Background check received from "..headers["User-Agent"]) 368 | '; 369 | } 370 | location = /foo { 371 | content_by_lua ' 372 | http:_http_background_func() 373 | '; 374 | } 375 | --- request 376 | GET /foo 377 | --- error_log: Background check received from Resty Upstream 378 | 379 | === TEST 7c: Default background check params are set - custom headers 380 | --- http_config eval 381 | "$::HttpConfig" 382 | ."$::InitConfig" 383 | . q{ 384 | test_api:add_host("primary", { 385 | id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, 386 | healthcheck = { 387 | path = "/check", 388 | headers = { 389 | ["X-Foo"] = "baz" 390 | } 391 | } 392 | }) 393 | '; 394 | } 395 | --- config 396 | location = /check { 397 | content_by_lua ' 398 | local headers = ngx.req.get_headers() 399 | ngx.log(ngx.ERR, "Background check received from "..headers["User-Agent"]) 400 | ngx.log(ngx.ERR, "X-Foo: "..headers["X-Foo"]) 401 | '; 402 | } 403 | location = /foo { 404 | content_by_lua ' 405 | http:_http_background_func() 406 | '; 407 | } 408 | --- request 409 | GET /foo 410 | --- error_log: Background check received from Resty Upstream 411 | --- error_log: X-Foo: baz 412 | 413 | === TEST 8: Healthcheck read timeout overrides pool 414 | --- http_config eval 415 | "$::HttpConfig" 416 | ."$::InitConfig" 417 | . q{ 418 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { path = "/check", read_timeout = 100 } }) 419 | '; 420 | } 421 | --- config 422 | location = / { 423 | content_by_lua ' 424 | ngx.update_time() 425 | local start = ngx.now() 426 | 427 | http:_http_background_func() 428 | 429 | ngx.update_time() 430 | local stop = ngx.now() 431 | local duration = stop - start 432 | 433 | -- Pool timeout is 1100 434 | if duration > 0.50 then 435 | ngx.say("Fail") 436 | else 437 | ngx.say("OK") 438 | end 439 | '; 440 | } 441 | location = /check { 442 | content_by_lua ' 443 | ngx.sleep(900) 444 | ngx.say("OK") 445 | '; 446 | } 447 | --- request 448 | GET / 449 | --- error_code: 200 450 | --- response_body 451 | OK 452 | 453 | === TEST 8b: Healthcheck read timeout doesn't affect frontend 454 | --- http_config eval 455 | "$::HttpConfig" 456 | ."$::InitConfig" 457 | . q{ 458 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { path = "/check", read_timeout = 100 } }) 459 | '; 460 | } 461 | --- config 462 | location = / { 463 | content_by_lua ' 464 | ngx.update_time() 465 | local start = ngx.now() 466 | 467 | local ok, err = http:request({path = "/check"}) 468 | 469 | ngx.update_time() 470 | local stop = ngx.now() 471 | local duration = stop - start 472 | 473 | -- Pool timeout is 1100 474 | if duration < 0.80 then 475 | ngx.say("Fail") 476 | else 477 | ngx.say("OK") 478 | end 479 | '; 480 | } 481 | location = /check { 482 | content_by_lua ' 483 | ngx.sleep(900) 484 | ngx.say("OK") 485 | '; 486 | } 487 | --- request 488 | GET / 489 | --- error_code: 200 490 | --- response_body 491 | OK 492 | 493 | === TEST 9: Healthcheck connect timeout overrides pool 494 | --- http_config eval 495 | "$::HttpConfig" 496 | ."$::InitConfig" 497 | . q{ 498 | test_api:add_host("primary", { id="a", host = "10.123.123.123", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { timeout = 10 } }) 499 | '; 500 | } 501 | --- config 502 | location = / { 503 | content_by_lua ' 504 | ngx.update_time() 505 | local start = ngx.now() 506 | 507 | http:_http_background_func() 508 | 509 | ngx.update_time() 510 | local stop = ngx.now() 511 | local duration = stop - start 512 | 513 | -- Pool timeout is 100 514 | if duration > 0.05 then 515 | ngx.say("Fail") 516 | else 517 | ngx.say("OK") 518 | end 519 | '; 520 | } 521 | --- request 522 | GET / 523 | --- error_code: 200 524 | --- response_body 525 | OK 526 | 527 | === TEST 9b: Healthcheck connect timeout doesn't affect frontend 528 | --- http_config eval 529 | "$::HttpConfig" 530 | ."$::InitConfig" 531 | . q{ 532 | test_api:add_host("primary", { id="a", host = "10.123.123.123", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { timeout = 10 } }) 533 | '; 534 | } 535 | --- config 536 | location = / { 537 | content_by_lua ' 538 | ngx.update_time() 539 | local start = ngx.now() 540 | 541 | local ok, err = http:request({path = "/check"}) 542 | 543 | ngx.update_time() 544 | local stop = ngx.now() 545 | local duration = stop - start 546 | 547 | -- Pool timeout is 100 548 | if duration < 0.05 then 549 | ngx.say("Fail") 550 | else 551 | ngx.say("OK") 552 | end 553 | '; 554 | } 555 | --- request 556 | GET / 557 | --- error_code: 200 558 | --- response_body 559 | OK 560 | 561 | 562 | === TEST 10: Healthcheck status_codes override pool 563 | --- http_config eval 564 | "$::HttpConfig" 565 | ."$::InitConfig" 566 | . q{ 567 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { path = "/check", status_codes = {["403"] = true} } }) 568 | '; 569 | } 570 | --- config 571 | location = / { 572 | content_by_lua ' 573 | http:_http_background_func() 574 | 575 | -- Run process_failed_hosts inline rather than after the request is done 576 | upstream._process_failed_hosts(false, upstream, upstream:ctx()) 577 | 578 | local pools, err = upstream:get_pools() 579 | local idx = upstream.get_host_idx("a", pools.primary.hosts) 580 | local host = pools.primary.hosts[idx] 581 | if host.failcount ~= 1 then 582 | ngx.status = 500 583 | end 584 | '; 585 | } 586 | location = /check { 587 | content_by_lua ' 588 | ngx.status = 403 589 | ngx.say("OK") 590 | '; 591 | } 592 | --- request 593 | GET / 594 | --- error_code: 200 595 | --- error_log: HTTP 403 from Host "a" 596 | 597 | === TEST 10b: Healthcheck status_codes doesn't affect frontend 598 | --- http_config eval 599 | "$::HttpConfig" 600 | ."$::InitConfig" 601 | . q{ 602 | test_api:add_host("primary", { id="a", host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1, healthcheck = { path = "/check", status_codes = {["403"] = true} } }) 603 | '; 604 | } 605 | --- config 606 | location = / { 607 | content_by_lua ' 608 | local res, err = http:request({path = "/check"}) 609 | if not res then 610 | ngx.status = err.status 611 | ngx.say(err.err) 612 | return ngx.exit(ngx.status) 613 | end 614 | ngx.say(res.status) 615 | '; 616 | } 617 | location = /check { 618 | content_by_lua ' 619 | ngx.status = 403 620 | ngx.say("OK") 621 | '; 622 | } 623 | --- request 624 | GET / 625 | --- error_code: 200 626 | --- no_error_log: HTTP 403 from Host "a" 627 | --- response_body 628 | 403 629 | -------------------------------------------------------------------------------- /t/11_hash.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * 44; 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | lua_shared_dict test_upstream 1m; 15 | 16 | 17 | }; 18 | 19 | our $InitConfig = qq{ 20 | init_by_lua ' 21 | cjson = require "cjson" 22 | upstream_socket = require("resty.upstream.socket") 23 | upstream_api = require("resty.upstream.api") 24 | 25 | upstream, configured = upstream_socket:new("test_upstream") 26 | test_api = upstream_api:new(upstream) 27 | 28 | test_api:create_pool({id = "primary", timeout = 100}) 29 | test_api:set_method("primary", "hash") 30 | 31 | }; 32 | 33 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 34 | 35 | no_long_string(); 36 | #no_diff(); 37 | 38 | run_tests(); 39 | 40 | __DATA__ 41 | 42 | === TEST 1: Hash method, single host 43 | --- http_config eval 44 | "$::HttpConfig" 45 | ."$::InitConfig" 46 | . q{ 47 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 48 | '; 49 | } 50 | --- log_level: debug 51 | --- config 52 | location = / { 53 | content_by_lua ' 54 | local sock, info = upstream:connect() 55 | if not sock then 56 | ngx.log(ngx.ERR, info) 57 | else 58 | ngx.say(info.host.id) 59 | sock:setkeepalive() 60 | end 61 | 62 | upstream:process_failed_hosts() 63 | '; 64 | } 65 | --- request 66 | GET / 67 | --- no_error_log 68 | [error] 69 | [warn] 70 | --- response_body 71 | 1 72 | 73 | === TEST 2: Hash between multiple hosts, default settings 74 | --- http_config eval 75 | "$::HttpConfig" 76 | ."$::InitConfig" 77 | . q{ 78 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 79 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 80 | '; 81 | } 82 | --- log_level: debug 83 | --- config 84 | location = / { 85 | content_by_lua ' 86 | local sock, info = upstream:connect(nil) 87 | if not sock then 88 | ngx.log(ngx.ERR, info) 89 | else 90 | ngx.say(info.host.id) 91 | sock:setkeepalive() 92 | end 93 | 94 | 95 | upstream:process_failed_hosts() 96 | '; 97 | } 98 | --- request 99 | GET / 100 | --- no_error_log 101 | [error] 102 | [warn] 103 | --- response_body 104 | 1 105 | 106 | === TEST 2b: Hash between multiple hosts, provide a user-defined key 107 | --- http_config eval 108 | "$::HttpConfig" 109 | ."$::InitConfig" 110 | . q{ 111 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 112 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 113 | '; 114 | } 115 | --- log_level: debug 116 | --- config 117 | location = / { 118 | content_by_lua ' 119 | local sock = nil 120 | local key = "1.2.3.4" -- i know this will hash to 2 from trial and error. hooray unit tests! 121 | 122 | local sock, info = upstream:connect(sock, key) 123 | if not sock then 124 | ngx.log(ngx.ERR, info) 125 | else 126 | ngx.say(info.host.id) 127 | sock:setkeepalive() 128 | end 129 | 130 | upstream:process_failed_hosts() 131 | '; 132 | } 133 | --- request 134 | GET / 135 | --- no_error_log 136 | [error] 137 | [warn] 138 | --- response_body 139 | 2 140 | 141 | === TEST 3: Hash is consistent 142 | --- http_config eval 143 | "$::HttpConfig" 144 | ."$::InitConfig" 145 | . q{ 146 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 147 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 148 | '; 149 | } 150 | --- log_level: debug 151 | --- config 152 | location = / { 153 | content_by_lua ' 154 | 155 | local count = 10 156 | for i=1,count do 157 | local sock, info = upstream:connect() 158 | if not sock then 159 | ngx.log(ngx.ERR, info) 160 | else 161 | ngx.print(info.host.id) 162 | sock:setkeepalive() 163 | end 164 | end 165 | 166 | upstream:process_failed_hosts() 167 | '; 168 | } 169 | --- request 170 | GET / 171 | --- no_error_log 172 | [error] 173 | [warn] 174 | --- response_body: 1111111111 175 | 176 | === TEST 4: Hash with user provided weights 177 | --- http_config eval 178 | "$::HttpConfig" 179 | ."$::InitConfig" 180 | . q{ 181 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 182 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 183 | '; 184 | } 185 | --- log_level: debug 186 | --- config 187 | location = / { 188 | content_by_lua ' 189 | 190 | local count = 10 191 | for i=1,count do 192 | local sock, info = upstream:connect() 193 | if not sock then 194 | ngx.log(ngx.ERR, info) 195 | else 196 | ngx.print(info.host.id) 197 | sock:setkeepalive() 198 | end 199 | end 200 | 201 | upstream:process_failed_hosts() 202 | '; 203 | } 204 | --- request 205 | GET / 206 | --- no_error_log 207 | [error] 208 | [warn] 209 | --- response_body: 1111111111 210 | 211 | === TEST 5: Weighted hash is consistent 212 | --- http_config eval 213 | "$::HttpConfig" 214 | ."$::InitConfig" 215 | . q{ 216 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 2 }) 217 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 218 | '; 219 | } 220 | --- log_level: debug 221 | --- config 222 | location = / { 223 | content_by_lua ' 224 | 225 | local count = 10 226 | for i=1,count do 227 | local sock, info = upstream:connect() 228 | if not sock then 229 | ngx.log(ngx.ERR, info) 230 | else 231 | ngx.print(info.host.id) 232 | sock:setkeepalive() 233 | end 234 | end 235 | 236 | upstream:process_failed_hosts() 237 | '; 238 | } 239 | --- request 240 | GET / 241 | --- no_error_log 242 | [error] 243 | [warn] 244 | --- response_body: 2222222222 245 | 246 | === TEST 5b: Weighted hash is consistent, odd number of hosts 247 | --- http_config eval 248 | "$::HttpConfig" 249 | ."$::InitConfig" 250 | . q{ 251 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 252 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 20 }) 253 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 30 }) 254 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 40 }) 255 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 50 }) 256 | '; 257 | } 258 | --- log_level: debug 259 | --- config 260 | location = / { 261 | content_by_lua ' 262 | 263 | local count = 20 264 | for i=1,count do 265 | local sock, info = upstream:connect() 266 | if not sock then 267 | ngx.log(ngx.ERR, info) 268 | else 269 | ngx.print(info.host.id) 270 | sock:setkeepalive() 271 | end 272 | end 273 | 274 | upstream:process_failed_hosts() 275 | '; 276 | } 277 | --- request 278 | GET / 279 | --- no_error_log 280 | [error] 281 | [warn] 282 | --- response_body: 33333333333333333333 283 | 284 | === TEST 5c: Weighted hash is consistent, last host has highest weight 285 | --- http_config eval 286 | "$::HttpConfig" 287 | ."$::InitConfig" 288 | . q{ 289 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 1 }) 290 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 2 }) 291 | '; 292 | } 293 | --- log_level: debug 294 | --- config 295 | location = / { 296 | content_by_lua ' 297 | 298 | local count = 10 299 | for i=1,count do 300 | local sock, info = upstream:connect() 301 | if not sock then 302 | ngx.log(ngx.ERR, info) 303 | else 304 | ngx.print(info.host.id) 305 | sock:setkeepalive() 306 | end 307 | end 308 | 309 | upstream:process_failed_hosts() 310 | '; 311 | } 312 | --- request 313 | GET / 314 | --- no_error_log 315 | [error] 316 | [warn] 317 | --- response_body: 2222222222 318 | 319 | === TEST 6: Weighted hash is consistent, divisable weights 320 | --- http_config eval 321 | "$::HttpConfig" 322 | ."$::InitConfig" 323 | . q{ 324 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 20 }) 325 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT, weight = 10 }) 326 | '; 327 | } 328 | --- log_level: debug 329 | --- config 330 | location = / { 331 | content_by_lua ' 332 | 333 | local count = 10 334 | for i=1,count do 335 | local sock, info = upstream:connect() 336 | if not sock then 337 | ngx.log(ngx.ERR, info) 338 | else 339 | ngx.print(info.host.id) 340 | sock:setkeepalive() 341 | end 342 | end 343 | 344 | upstream:process_failed_hosts() 345 | '; 346 | } 347 | --- request 348 | GET / 349 | --- no_error_log 350 | [error] 351 | [warn] 352 | --- response_body: 1111111111 353 | 354 | === TEST 7: Hash is consistent, re-keyed, and consistent 355 | --- http_config eval 356 | "$::HttpConfig" 357 | ."$::InitConfig" 358 | . q{ 359 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 360 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 361 | '; 362 | } 363 | --- log_level: debug 364 | --- config 365 | location = / { 366 | content_by_lua ' 367 | 368 | local count = 10 369 | for i=1,count do 370 | local sock, info = upstream:connect() 371 | if not sock then 372 | ngx.log(ngx.ERR, info) 373 | else 374 | ngx.print(info.host.id) 375 | sock:setkeepalive() 376 | end 377 | end 378 | 379 | upstream:process_failed_hosts() 380 | 381 | test_api:down_host("primary", 1) 382 | 383 | local count = 10 384 | for i=1,count do 385 | local sock, info = upstream:connect() 386 | if not sock then 387 | ngx.log(ngx.ERR, info) 388 | else 389 | ngx.print(info.host.id) 390 | sock:setkeepalive() 391 | end 392 | end 393 | '; 394 | } 395 | 396 | --- request 397 | GET / 398 | --- no_error_log 399 | [error] 400 | [warn] 401 | --- response_body: 11111111112222222222 402 | 403 | === TEST 7b: Hash is consistent, re-keyed, consistent, re-keyed again, and consistent 404 | --- http_config eval 405 | "$::HttpConfig" 406 | ."$::InitConfig" 407 | . q{ 408 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 409 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 410 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 411 | test_api:add_host("primary", { host = "127.0.0.1", port = $TEST_NGINX_SERVER_PORT }) 412 | '; 413 | } 414 | --- log_level: debug 415 | --- config 416 | location = / { 417 | content_by_lua ' 418 | 419 | local count = 10 420 | for i=1,count do 421 | local sock, info = upstream:connect() 422 | if not sock then 423 | ngx.log(ngx.ERR, info) 424 | else 425 | ngx.print(info.host.id) 426 | sock:setkeepalive() 427 | end 428 | end 429 | 430 | test_api:down_host("primary", 1) 431 | 432 | upstream:process_failed_hosts() 433 | 434 | local count = 10 435 | for i=1,count do 436 | local sock, info = upstream:connect() 437 | if not sock then 438 | ngx.log(ngx.ERR, info) 439 | else 440 | ngx.print(info.host.id) 441 | sock:setkeepalive() 442 | end 443 | end 444 | 445 | upstream:process_failed_hosts() 446 | 447 | test_api:up_host("primary", 1) 448 | 449 | local count = 10 450 | for i=1,count do 451 | local sock, info = upstream:connect() 452 | if not sock then 453 | ngx.log(ngx.ERR, info) 454 | else 455 | ngx.print(info.host.id) 456 | sock:setkeepalive() 457 | end 458 | end 459 | '; 460 | } 461 | 462 | --- request 463 | GET / 464 | --- no_error_log 465 | [error] 466 | [warn] 467 | --- response_body: 111111111144444444441111111111 468 | 469 | -------------------------------------------------------------------------------- /util/lua-releng.pl: -------------------------------------------------------------------------------- 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 | 11 | 12 | print "Checking use of Lua global variables in file $file ...\n"; 13 | 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|rawget|rawset|rawlen'"); 14 | file_contains($file, "attempt to write to undeclared variable"); 15 | #system("grep -H -n -E --color '.{81}' $file"); 16 | } 17 | 18 | sub file_contains ($$) { 19 | my ($file, $regex) = @_; 20 | open my $in, $file 21 | or die "Cannot open $file fo reading: $!\n"; 22 | my $content = do { local $/; <$in> }; 23 | close $in; 24 | #print "$content"; 25 | return scalar ($content =~ /$regex/); 26 | } 27 | 28 | if (-d 't') { 29 | for my $file (map glob, qw{ t/*.t t/*/*.t t/*/*/*.t }) { 30 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 31 | } 32 | } 33 | --------------------------------------------------------------------------------