├── .gitattributes ├── .gitignore ├── .travis.yml ├── Makefile ├── README.markdown ├── chash.c ├── chash.h ├── lib └── resty │ ├── balancer │ └── utils.lua │ ├── chash.lua │ ├── roundrobin.lua │ └── swrr.lua ├── t ├── bench-chash.lua ├── bench-roundrobin.lua ├── chash.t ├── roundrobin.t └── swrr.t └── valgrind.suppress /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | t/servroot 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: trusty 3 | 4 | sudo: required 5 | 6 | addons: 7 | apt: 8 | packages: 9 | - axel 10 | - build-essential 11 | - cpanminus 12 | - libgd-dev 13 | - liblist-moreutils-perl 14 | - libncurses5-dev 15 | - libpcre3-dev 16 | - libreadline-dev 17 | - libssl-dev 18 | - libtest-base-perl 19 | - libtest-longstring-perl 20 | - libtext-diff-perl 21 | - liburi-perl 22 | - libwww-perl 23 | - perl 24 | - valgrind 25 | 26 | cache: 27 | directories: 28 | - download-cache 29 | 30 | env: 31 | global: 32 | - JOBS=2 33 | - OPENRESTY_PREFIX=/usr/local/openresty 34 | - OPENRESTY_VER=1.19.9.1 35 | jobs: 36 | - TEST_NGINX_USE_VALGRIND=0 37 | - TEST_NGINX_USE_VALGRIND=1 38 | 39 | install: 40 | - if [ ! -f download-cache/openresty-$OPENRESTY_VER.tar.gz ]; then 41 | wget -P download-cache https://openresty.org/download/openresty-$OPENRESTY_VER.tar.gz; 42 | fi 43 | - git clone https://github.com/openresty/test-nginx.git ../test-nginx 44 | 45 | # install openresty-openssl111-dev 46 | - sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates 47 | - wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add - 48 | - echo "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main" 49 | | sudo tee /etc/apt/sources.list.d/openresty.list 50 | - sudo apt-get update || echo 'apt-get update failed, but ignore it' 51 | - sudo apt-get -y install --no-install-recommends openresty-openssl111 openresty-openssl111-dev 52 | 53 | script: 54 | - if [ TEST_NGINX_USE_VALGRIND = 1 ]; then export luajit_xcflags='-DLUAJIT_USE_VALGRIND -DLUAJIT_USE_SYSMALLOC'; fi 55 | - tar xzf download-cache/openresty-$OPENRESTY_VER.tar.gz && 56 | cd openresty-$OPENRESTY_VER 57 | - ./configure --prefix=$OPENRESTY_PREFIX 58 | --with-cc-opt="-I/usr/local/openresty/openssl111/include" 59 | --with-ld-opt="-L/usr/local/openresty/openssl111/lib -Wl,-rpath,/usr/local/openresty/openssl111/lib" 60 | --with-luajit-xcflags="$luajit_xcflags" 61 | -j$JOBS 62 | > build.log 2>&1 || (cat build.log && exit 1) 63 | - make -j$JOBS > build.log 2>&1 || 64 | (cat build.log && exit 1) 65 | - sudo make install > build.log 2>&1 || 66 | (cat build.log && exit 1) 67 | - cd .. 68 | - export PATH=$OPENRESTY_PREFIX/nginx/sbin:$PATH 69 | - make test jobs=$JOBS > build.log 2>&1 || 70 | (cat build.log && exit 1) 71 | - cat build.log 72 | - if [ `grep -c '== Invalid' build.log` -gt 0 ]; then echo 'valgrind complaining' && exit 1; fi 73 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty-debug 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | 8 | .PHONY: all test install bench 9 | 10 | SRC := chash.c 11 | OBJ := $(SRC:.c=.o) 12 | 13 | C_SO_NAME := librestychash.so 14 | 15 | CFLAGS := -O3 -g -Wall -fpic 16 | 17 | LDFLAGS := -shared 18 | # on Mac OS X, one should set instead: 19 | # LDFLAGS := -bundle -undefined dynamic_lookup 20 | 21 | ifeq ($(shell uname),Darwin) 22 | LDFLAGS := -bundle -undefined dynamic_lookup 23 | C_SO_NAME := librestychash.dylib 24 | endif 25 | 26 | MY_CFLAGS := $(CFLAGS) -DBUILDING_SO 27 | MY_LDFLAGS := $(LDFLAGS) -fvisibility=hidden 28 | 29 | test := t 30 | 31 | .PHONY = all test clean install 32 | 33 | all : $(C_SO_NAME) 34 | 35 | ${OBJ} : %.o : %.c 36 | $(CC) $(MY_CFLAGS) -c $< 37 | 38 | ${C_SO_NAME} : ${OBJ} 39 | $(CC) $(MY_LDFLAGS) $^ -o $@ 40 | 41 | #export TEST_NGINX_NO_CLEAN=1 42 | 43 | clean:; rm -f *.o *.so a.out *.d 44 | 45 | install: 46 | $(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty 47 | $(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty/balancer 48 | $(INSTALL) lib/resty/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty 49 | $(INSTALL) lib/resty/balancer/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/balancer 50 | $(INSTALL) $(C_SO_NAME) $(DESTDIR)$(LUA_LIB_DIR)/ 51 | 52 | test : all 53 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(test) 54 | 55 | bench: 56 | $(OPENRESTY_PREFIX)/bin/resty t/bench-chash.lua `pwd` 57 | $(OPENRESTY_PREFIX)/bin/resty t/bench-roundrobin.lua `pwd` 58 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-chash - A generic consistent hash implementation for OpenResty/LuaJIT 5 | 6 | lua-resty-roundrobin - A generic roundrobin implementation for OpenResty/LuaJIT 7 | 8 | Table of Contents 9 | ================= 10 | 11 | * [Name](#name) 12 | * [Status](#status) 13 | * [Description](#description) 14 | * [Synopsis](#synopsis) 15 | * [Methods](#methods) 16 | * [new](#new) 17 | * [reinit](#reinit) 18 | * [set](#set) 19 | * [delete](#delete) 20 | * [incr](#incr) 21 | * [decr](#decr) 22 | * [find](#find) 23 | * [next](#next) 24 | * [Installation](#installation) 25 | * [Performance](#performance) 26 | * [Author](#author) 27 | * [Copyright and License](#copyright-and-license) 28 | * [See Also](#see-also) 29 | 30 | Status 31 | ====== 32 | 33 | This library is still under early development and is still experimental. 34 | 35 | Description 36 | =========== 37 | 38 | This Lua library can be used with `balancer_by_lua*`. 39 | 40 | Synopsis 41 | ======== 42 | 43 | ```lua 44 | lua_package_path "/path/to/lua-resty-chash/lib/?.lua;;"; 45 | lua_package_cpath "/path/to/lua-resty-chash/?.so;;"; 46 | 47 | init_by_lua_block { 48 | local resty_chash = require "resty.chash" 49 | local resty_roundrobin = require "resty.roundrobin" 50 | local resty_swrr = require "resty.swrr" 51 | 52 | local server_list = { 53 | ["127.0.0.1:1985"] = 2, 54 | ["127.0.0.1:1986"] = 2, 55 | ["127.0.0.1:1987"] = 1, 56 | } 57 | 58 | -- XX: we can do the following steps to keep consistency with nginx chash 59 | local str_null = string.char(0) 60 | 61 | local servers, nodes = {}, {} 62 | for serv, weight in pairs(server_list) do 63 | -- XX: we can just use serv as id when we doesn't need keep consistency with nginx chash 64 | local id = string.gsub(serv, ":", str_null) 65 | 66 | servers[id] = serv 67 | nodes[id] = weight 68 | end 69 | 70 | local chash_up = resty_chash:new(nodes) 71 | 72 | package.loaded.my_chash_up = chash_up 73 | package.loaded.my_servers = servers 74 | 75 | local rr_up = resty_roundrobin:new(server_list) 76 | package.loaded.my_rr_up = rr_up 77 | 78 | local swrr_up = resty_swrr:new(server_list) 79 | package.loaded.my_swrr_up = swrr_up 80 | } 81 | 82 | upstream backend_chash { 83 | server 0.0.0.1; 84 | balancer_by_lua_block { 85 | local b = require "ngx.balancer" 86 | 87 | local chash_up = package.loaded.my_chash_up 88 | local servers = package.loaded.my_servers 89 | 90 | -- we can balancer by any key here 91 | local id = chash_up:find(ngx.var.arg_key) 92 | local server = servers[id] 93 | 94 | assert(b.set_current_peer(server)) 95 | } 96 | } 97 | 98 | upstream backend_rr { 99 | server 0.0.0.1; 100 | balancer_by_lua_block { 101 | local b = require "ngx.balancer" 102 | 103 | local rr_up = package.loaded.my_rr_up 104 | 105 | -- Note that Round Robin picks the first server randomly 106 | local server = rr_up:find() 107 | 108 | assert(b.set_current_peer(server)) 109 | } 110 | } 111 | 112 | upstream backend_swrr { 113 | server 0.0.0.1; 114 | balancer_by_lua_block { 115 | local b = require "ngx.balancer" 116 | 117 | local swrr_up = package.loaded.my_swrr_up 118 | 119 | -- Note that SWRR picks the first server randomly 120 | local server = swrr_up:find() 121 | 122 | assert(b.set_current_peer(server)) 123 | } 124 | } 125 | 126 | server { 127 | location /chash { 128 | proxy_pass http://backend_chash; 129 | } 130 | 131 | location /roundrobin { 132 | proxy_pass http://backend_rr; 133 | } 134 | 135 | location /swrr { 136 | proxy_pass http://backend_swrr; 137 | } 138 | } 139 | ``` 140 | 141 | [Back to TOC](#table-of-contents) 142 | 143 | Methods 144 | ======= 145 | 146 | Both `resty.chash`, `resty.roundrobin` and `resty.swrr` have the same apis. 147 | 148 | [Back to TOC](#table-of-contents) 149 | 150 | new 151 | --- 152 | **syntax:** `obj, err = class.new(nodes)` 153 | 154 | Instantiates an object of this class. The `class` value is returned by the call `require "resty.chash"`. 155 | 156 | The `id` should be `table.concat({host, string.char(0), port})` like the nginx chash does, 157 | when we need to keep consistency with nginx chash. 158 | 159 | The `id` can be any string value when we do not need to keep consistency with nginx chash. 160 | The `weight` should be a non negative integer. 161 | 162 | ```lua 163 | local nodes = { 164 | -- id => weight 165 | server1 = 10, 166 | server2 = 2, 167 | } 168 | 169 | local resty_chash = require "resty.chash" 170 | 171 | local chash = resty_chash:new(nodes) 172 | 173 | local id = chash:find("foo") 174 | 175 | ngx.say(id) 176 | ``` 177 | 178 | [Back to TOC](#table-of-contents) 179 | 180 | reinit 181 | -------- 182 | **syntax:** `obj:reinit(nodes)` 183 | 184 | Reinit the chash obj with the new `nodes`. 185 | 186 | [Back to TOC](#table-of-contents) 187 | 188 | set 189 | -------- 190 | **syntax:** `obj:set(id, weight)` 191 | 192 | Set `weight` of the `id`. 193 | 194 | [Back to TOC](#table-of-contents) 195 | 196 | delete 197 | -------- 198 | **syntax:** `obj:delete(id)` 199 | 200 | Delete the `id`. 201 | 202 | [Back to TOC](#table-of-contents) 203 | 204 | incr 205 | -------- 206 | **syntax:** `obj:incr(id, weight?)` 207 | 208 | Increments weight for the `id` by the step value `weight`(default to 1). 209 | 210 | [Back to TOC](#table-of-contents) 211 | 212 | decr 213 | -------- 214 | **syntax:** `obj:decr(id, weight?)` 215 | 216 | Decrease weight for the `id` by the step value `weight`(default to 1). 217 | 218 | [Back to TOC](#table-of-contents) 219 | 220 | find 221 | -------- 222 | **syntax:** `id, index = obj:find(key)` 223 | 224 | Find an id by the `key`, same key always return the same `id` in the same `obj`. 225 | 226 | The second return value `index` is the index in the chash circle of the hash value of the `key`. 227 | 228 | [Back to TOC](#table-of-contents) 229 | 230 | next 231 | -------- 232 | **syntax:** `id, new_index = obj:next(old_index)` 233 | 234 | If we have chance to retry when the first `id`(server) doesn't work well, 235 | then we can use `obj:next` to get the next `id`. 236 | 237 | The new `id` may be the same as the old one. 238 | 239 | [Back to TOC](#table-of-contents) 240 | 241 | Installation 242 | ============ 243 | 244 | First you need to run `make` to generate the librestychash.so. 245 | Then you need to configure the lua_package_path and lua_package_cpath directive 246 | to add the path of your lua-resty-chash source tree to ngx_lua's LUA_PATH search 247 | path, as in 248 | 249 | ```nginx 250 | # nginx.conf 251 | http { 252 | lua_package_path "/path/to/lua-resty-chash/lib/?.lua;;"; 253 | lua_package_cpath "/path/to/lua-resty-chash/?.so;;"; 254 | ... 255 | } 256 | ``` 257 | 258 | Ensure that the system account running your Nginx ''worker'' proceses have 259 | enough permission to read the `.lua` and `.so` file. 260 | 261 | [Back to TOC](#table-of-contents) 262 | 263 | Performance 264 | =========== 265 | 266 | There is a benchmark script `t/bench.lua`. 267 | 268 | I got the result when I run `make bench`: 269 | 270 | ``` 271 | chash new servers 272 | 10000 times 273 | elasped: 0.61600017547607 274 | 275 | chash new servers2 276 | 1000 times 277 | elasped: 0.77300000190735 278 | 279 | chash new servers3 280 | 10000 times 281 | elasped: 0.66899991035461 282 | 283 | new in func 284 | 10000 times 285 | elasped: 0.62000012397766 286 | 287 | new dynamic 288 | 10000 times 289 | elasped: 0.75499987602234 290 | 291 | incr server3 292 | 10000 times 293 | elasped: 0.19000029563904 294 | 295 | incr server1 296 | 10000 times 297 | elasped: 0.33699989318848 298 | 299 | decr server1 300 | 10000 times 301 | elasped: 0.27300024032593 302 | 303 | delete server3 304 | 10000 times 305 | elasped: 0.037999868392944 306 | 307 | delete server1 308 | 10000 times 309 | elasped: 0.065000057220459 310 | 311 | set server1 9 312 | 10000 times 313 | elasped: 0.26600003242493 314 | 315 | set server1 8 316 | 10000 times 317 | elasped: 0.32000017166138 318 | 319 | set server1 1 320 | 10000 times 321 | elasped: 0.56699991226196 322 | 323 | base for find 324 | 1000000 times 325 | elasped: 0.01800012588501 326 | 327 | find 328 | 1000000 times 329 | elasped: 0.9469997882843 330 | ``` 331 | 332 | [Back to TOC](#table-of-contents) 333 | 334 | Author 335 | ====== 336 | 337 | Dejiang Zhu (doujiang24) . 338 | 339 | [Back to TOC](#table-of-contents) 340 | 341 | Copyright and License 342 | ===================== 343 | 344 | This module is licensed under the BSD license. 345 | 346 | Copyright (C) 2015-2016, by Yichun Zhang (agentzh) , CloudFlare Inc. 347 | 348 | All rights reserved. 349 | 350 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 351 | 352 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 353 | 354 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 355 | 356 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 357 | 358 | [Back to TOC](#table-of-contents) 359 | 360 | See Also 361 | ======== 362 | * the ngx_lua module: http://wiki.nginx.org/HttpLuaModule 363 | * the json lib for Lua and C: https://github.com/cloudflare/lua-resty-json 364 | 365 | [Back to TOC](#table-of-contents) 366 | 367 | -------------------------------------------------------------------------------- /chash.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "chash.h" 7 | 8 | 9 | #ifndef u_char 10 | #define u_char unsigned char 11 | #endif 12 | 13 | #define CHASH_OK 0 14 | #define CHASH_ERR -1 15 | 16 | #define crc32_final(crc) \ 17 | crc ^= 0xffffffff 18 | 19 | 20 | static uint32_t crc32_table256[] = { 21 | 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 22 | 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 23 | 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 24 | 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 25 | 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 26 | 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 27 | 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 28 | 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 29 | 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 30 | 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 31 | 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 32 | 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 33 | 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 34 | 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 35 | 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 36 | 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 37 | 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 38 | 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 39 | 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 40 | 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 41 | 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 42 | 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 43 | 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 44 | 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 45 | 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 46 | 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 47 | 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 48 | 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 49 | 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 50 | 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 51 | 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 52 | 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 53 | 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 54 | 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 55 | 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 56 | 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 57 | 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 58 | 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 59 | 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 60 | 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 61 | 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 62 | 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 63 | 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 64 | 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 65 | 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 66 | 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 67 | 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 68 | 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 69 | 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 70 | 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 71 | 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 72 | 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 73 | 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 74 | 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 75 | 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 76 | 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 77 | 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 78 | 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 79 | 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 80 | 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 81 | 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 82 | 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 83 | 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 84 | 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d 85 | }; 86 | 87 | 88 | static inline void 89 | crc32_update(uint32_t *crc, u_char *p, size_t len) 90 | { 91 | uint32_t c; 92 | 93 | c = *crc; 94 | 95 | while (len--) { 96 | c = crc32_table256[(c ^ *p++) & 0xff] ^ (c >> 8); 97 | } 98 | 99 | *crc = c; 100 | } 101 | 102 | 103 | static inline void 104 | chash_point_init_crc(chash_point_t *arr, uint32_t start, uint32_t base_hash, 105 | uint32_t from, uint32_t num, uint32_t id) 106 | { 107 | chash_point_t *node; 108 | uint32_t i, hash; 109 | union { 110 | uint32_t value; 111 | u_char byte[4]; 112 | } prev_hash; 113 | 114 | prev_hash.value = 0; 115 | node = &arr[start]; 116 | 117 | for (i = 0; i < from + num; i++) { 118 | hash = base_hash; 119 | crc32_update(&hash, prev_hash.byte, 4); 120 | crc32_final(hash); 121 | 122 | if (i >= from) { 123 | node->hash = hash; 124 | node->id = id; 125 | node = node + 1; 126 | } 127 | 128 | /* no big performace different in my test */ 129 | 130 | /* this only works when have little endian */ 131 | // prev_hash.value = hash; 132 | 133 | prev_hash.byte[0] = (u_char) (hash & 0xff); 134 | prev_hash.byte[1] = (u_char) ((hash >> 8) & 0xff); 135 | prev_hash.byte[2] = (u_char) ((hash >> 16) & 0xff); 136 | prev_hash.byte[3] = (u_char) ((hash >> 24) & 0xff); 137 | } 138 | } 139 | 140 | 141 | void 142 | chash_point_init(chash_point_t *arr, uint32_t base_hash, uint32_t start, 143 | uint32_t num, uint32_t id) 144 | { 145 | chash_point_init_crc(arr, start, base_hash, 0, num, id); 146 | } 147 | 148 | 149 | int 150 | chash_point_sort(chash_point_t arr[], uint32_t n) 151 | { 152 | chash_point_t *points; 153 | chash_point_t *node; 154 | int i, j, index, start, end; 155 | uint32_t min_sz, m, step; 156 | 157 | /* not sure 1.6 is the best */ 158 | min_sz = n * 1.6; 159 | m = 2; 160 | 161 | while (m <= min_sz) { 162 | m *= 2; 163 | } 164 | 165 | step = pow(2, 32) / m; 166 | 167 | points = (chash_point_t *) calloc(m, sizeof(chash_point_t)); 168 | if (points == NULL) { 169 | return CHASH_ERR; 170 | } 171 | 172 | for (i = 0; i < n; i++) { 173 | node = &arr[i]; 174 | index = node->hash / step; 175 | 176 | assert(index < m); // index must less than m 177 | 178 | for (end = index; end >= 0; end--) { 179 | if (points[end].id == 0) { 180 | goto insert; 181 | } 182 | 183 | if (node->hash >= points[end].hash) { 184 | break; 185 | } 186 | } 187 | 188 | for (start = end - 1; start >= 0; start--) { 189 | if (points[start].id == 0) { 190 | /* left shift before end */ 191 | for (j = start; j < end; j++) { 192 | points[j].hash = points[j + 1].hash; 193 | points[j].id = points[j + 1].id; 194 | } 195 | 196 | /* points[end] is empty now */ 197 | 198 | /* left shift after end when node->hash is bigger than them */ 199 | /* only end == index can match this */ 200 | while (end + 1 < m 201 | && points[end + 1].id != 0 202 | && points[end + 1].hash < node->hash) 203 | { 204 | points[end].hash = points[end + 1].hash; 205 | points[end].id = points[end + 1].id; 206 | end += 1; 207 | } 208 | 209 | goto insert; 210 | } 211 | } 212 | 213 | /* full before index, try to append */ 214 | 215 | for (end = end + 1; end < m; end++) { 216 | if (points[end].id == 0) { 217 | goto insert; 218 | } 219 | 220 | if (node->hash < points[end].hash) { 221 | break; 222 | } 223 | } 224 | 225 | for (start = end + 1; start < m; start++) { 226 | if (points[start].id == 0) { 227 | break; 228 | } 229 | } 230 | 231 | /* right shift */ 232 | for (j = start; j > end; j--) { 233 | points[j].hash = points[j - 1].hash; 234 | points[j].id = points[j - 1].id; 235 | } 236 | 237 | insert: 238 | assert(end < m && end >= 0); 239 | 240 | points[end].id = node->id; 241 | points[end].hash = node->hash; 242 | } 243 | 244 | j = 0; 245 | for (i = 0; i < m; i++) { 246 | if (points[i].id != 0) { 247 | arr[j].id = points[i].id; 248 | arr[j].hash = points[i].hash; 249 | j++; 250 | } 251 | } 252 | 253 | free(points); 254 | 255 | return CHASH_OK; 256 | } 257 | 258 | 259 | int 260 | chash_point_add(chash_point_t *old_points, uint32_t old_length, 261 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id, 262 | chash_point_t *new_points) 263 | { 264 | int i, j, k; 265 | chash_point_t *tmp_points; 266 | 267 | tmp_points = (chash_point_t *) calloc(num, sizeof(chash_point_t)); 268 | if (tmp_points == NULL) { 269 | return CHASH_ERR; 270 | } 271 | 272 | chash_point_init_crc(tmp_points, 0, base_hash, from, num, id); 273 | 274 | if (chash_point_sort(tmp_points, num) != CHASH_OK) { 275 | free(tmp_points); 276 | return CHASH_ERR; 277 | } 278 | 279 | j = num - 1; 280 | k = old_length + num - 1; 281 | for (i = old_length - 1; i >= 0; i--, k--) { 282 | while (j >= 0 && tmp_points[j].hash > old_points[i].hash) { 283 | new_points[k].hash = tmp_points[j].hash; 284 | new_points[k].id = tmp_points[j].id; 285 | 286 | j--; 287 | k--; 288 | } 289 | 290 | new_points[k].hash = old_points[i].hash; 291 | new_points[k].id = old_points[i].id; 292 | } 293 | 294 | for (; j >= 0; j--, k--) { 295 | new_points[k].hash = tmp_points[j].hash; 296 | new_points[k].id = tmp_points[j].id; 297 | } 298 | 299 | free(tmp_points); 300 | 301 | return CHASH_OK; 302 | } 303 | 304 | 305 | int 306 | chash_point_reduce(chash_point_t *old_points, uint32_t old_length, 307 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id) 308 | { 309 | int i, j, k; 310 | chash_point_t *tmp_points; 311 | 312 | tmp_points = (chash_point_t *) calloc(num, sizeof(chash_point_t)); 313 | 314 | chash_point_init_crc(tmp_points, 0, base_hash, from, num, id); 315 | 316 | if (chash_point_sort(tmp_points, num) != CHASH_OK) { 317 | free(tmp_points); 318 | return CHASH_ERR; 319 | } 320 | 321 | for (i = 0, j = 0, k = 0; i < old_length; i++) { 322 | if (j < num 323 | && old_points[i].hash == tmp_points[j].hash 324 | && old_points[i].id == id) 325 | { 326 | j++; 327 | continue; 328 | } 329 | 330 | if (i != k) { 331 | old_points[k].hash = old_points[i].hash; 332 | old_points[k].id = old_points[i].id; 333 | } 334 | k++; 335 | } 336 | 337 | free(tmp_points); 338 | 339 | return CHASH_OK; 340 | } 341 | 342 | 343 | void 344 | chash_point_delete(chash_point_t *old_points, uint32_t old_length, uint32_t id) 345 | { 346 | int i, j; 347 | 348 | for (i = 0, j = 0; i < old_length; i++) { 349 | if (old_points[i].id == id) { 350 | continue; 351 | } 352 | 353 | if (i != j) { 354 | old_points[j].hash = old_points[i].hash; 355 | old_points[j].id = old_points[i].id; 356 | } 357 | j++; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /chash.h: -------------------------------------------------------------------------------- 1 | /* ************************************************************************** 2 | * 3 | * borrow from: github.com/cloudflare/lua-resty-json 4 | * 5 | **/ 6 | #ifndef LUA_RESTY_CJSON_H 7 | #define LUA_RESTY_CJSON_H 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | typedef struct { 19 | uint32_t hash; 20 | uint32_t id; 21 | } chash_point_t; 22 | 23 | #ifdef BUILDING_SO 24 | #ifndef __APPLE__ 25 | #define LCH_EXPORT __attribute__ ((visibility ("protected"))) 26 | #else 27 | /* OSX does not support protect-visibility */ 28 | #define LCH_EXPORT __attribute__ ((visibility ("default"))) 29 | #endif 30 | #else 31 | #define LCH_EXPORT 32 | #endif 33 | 34 | /* ************************************************************************** 35 | * 36 | * Export Functions 37 | * 38 | * ************************************************************************** 39 | */ 40 | void chash_point_init(chash_point_t *points, uint32_t base_hash, 41 | uint32_t start, uint32_t num, uint32_t id) LCH_EXPORT; 42 | int chash_point_sort(chash_point_t *points, uint32_t npoints) LCH_EXPORT; 43 | 44 | int chash_point_add(chash_point_t *old_points, uint32_t old_length, 45 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id, 46 | chash_point_t *new_points) LCH_EXPORT; 47 | int chash_point_reduce(chash_point_t *old_points, uint32_t old_length, 48 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id) LCH_EXPORT; 49 | void chash_point_delete(chash_point_t *old_points, uint32_t old_length, 50 | uint32_t id) LCH_EXPORT; 51 | 52 | #ifdef __cplusplus 53 | } 54 | #endif 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /lib/resty/balancer/utils.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | _M.name = "balancer-utils" 4 | _M.version = "0.03" 5 | 6 | local new_tab 7 | do 8 | local ok 9 | ok, new_tab = pcall(require, "table.new") 10 | if not ok or type(new_tab) ~= "function" then 11 | new_tab = function (narr, nrec) return {} end 12 | end 13 | end 14 | _M.new_tab = new_tab 15 | 16 | 17 | local nkeys, tab_nkeys 18 | do 19 | local ok 20 | ok, nkeys = pcall(require, "table.nkeys") 21 | if not ok or type(nkeys) ~= "function" then 22 | nkeys = function(tab) 23 | local count = 0 24 | for _, _ in pairs(tab) do 25 | count = count + 1 26 | end 27 | return count 28 | end 29 | 30 | else 31 | tab_nkeys = nkeys 32 | end 33 | end 34 | _M.nkeys = nkeys 35 | 36 | 37 | function _M.copy(nodes) 38 | local newnodes = new_tab(0, tab_nkeys and tab_nkeys(nodes) or 4) 39 | for id, weight in pairs(nodes) do 40 | newnodes[id] = tonumber(weight) 41 | end 42 | 43 | return newnodes 44 | end 45 | 46 | 47 | return _M 48 | -------------------------------------------------------------------------------- /lib/resty/chash.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- some of them borrow from https://github.com/cloudflare/lua-resty-json 3 | -- 4 | 5 | local bit = require "bit" 6 | local ffi = require 'ffi' 7 | 8 | local utils = require "resty.balancer.utils" 9 | 10 | local new_tab = utils.new_tab 11 | local copy = utils.copy 12 | 13 | local ffi_new = ffi.new 14 | local C = ffi.C 15 | local crc32 = ngx.crc32_short 16 | local setmetatable = setmetatable 17 | local floor = math.floor 18 | local pairs = pairs 19 | local tostring = tostring 20 | local tonumber = tonumber 21 | local bxor = bit.bxor 22 | local error = error 23 | 24 | 25 | local CHASH_OK = 0 26 | 27 | 28 | ffi.cdef[[ 29 | typedef unsigned int uint32_t; 30 | 31 | typedef struct { 32 | uint32_t hash; 33 | uint32_t id; 34 | } chash_point_t; 35 | 36 | void chash_point_init(chash_point_t *points, uint32_t base_hash, uint32_t start, 37 | uint32_t num, uint32_t id); 38 | int chash_point_sort(chash_point_t *points, uint32_t size); 39 | 40 | int chash_point_add(chash_point_t *old_points, uint32_t old_length, 41 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id, 42 | chash_point_t *new_points); 43 | int chash_point_reduce(chash_point_t *old_points, uint32_t old_length, 44 | uint32_t base_hash, uint32_t from, uint32_t num, uint32_t id); 45 | void chash_point_delete(chash_point_t *old_points, uint32_t old_length, 46 | uint32_t id); 47 | ]] 48 | 49 | -- 50 | -- Find shared object file package.cpath, obviating the need of setting 51 | -- LD_LIBRARY_PATH 52 | -- Or we should add a little patch for ffi.load ? 53 | -- 54 | local function load_shared_lib(so_name) 55 | local string_gmatch = string.gmatch 56 | local string_match = string.match 57 | local io_open = io.open 58 | local io_close = io.close 59 | 60 | local cpath = package.cpath 61 | 62 | local postfix = ".so" 63 | if ffi.os == "OSX" then 64 | postfix = ".dylib" 65 | end 66 | so_name = so_name .. postfix 67 | 68 | for k, _ in string_gmatch(cpath, "[^;]+") do 69 | local fpath = string_match(k, "(.*/)") 70 | fpath = fpath .. so_name 71 | 72 | -- Don't get me wrong, the only way to know if a file exist is trying 73 | -- to open it. 74 | local f = io_open(fpath) 75 | if f ~= nil then 76 | io_close(f) 77 | return ffi.load(fpath) 78 | end 79 | end 80 | end 81 | 82 | 83 | local _M = {} 84 | local mt = { __index = _M } 85 | 86 | 87 | local clib = load_shared_lib("librestychash") 88 | if not clib then 89 | error("can not load librestychash") 90 | end 91 | 92 | local CONSISTENT_POINTS = 160 -- points per server 93 | local pow32 = math.pow(2, 32) 94 | 95 | local chash_point_t = ffi.typeof("chash_point_t[?]") 96 | 97 | 98 | local function _precompute(nodes) 99 | local n, total_weight = 0, 0 100 | for id, weight in pairs(nodes) do 101 | n = n + 1 102 | total_weight = total_weight + weight 103 | end 104 | 105 | local newnodes = copy(nodes) 106 | 107 | local ids = new_tab(n, 0) 108 | local npoints = total_weight * CONSISTENT_POINTS 109 | local points = ffi_new(chash_point_t, npoints) 110 | 111 | local start, index = 0, 0 112 | for id, weight in pairs(nodes) do 113 | local num = weight * CONSISTENT_POINTS 114 | local base_hash = bxor(crc32(tostring(id)), 0xffffffff) 115 | 116 | index = index + 1 117 | ids[index] = id 118 | 119 | clib.chash_point_init(points, base_hash, start, num, index) 120 | 121 | start = start + num 122 | end 123 | 124 | if clib.chash_point_sort(points, npoints) ~= CHASH_OK then 125 | error("no memory") 126 | end 127 | 128 | return ids, points, npoints, newnodes 129 | end 130 | 131 | 132 | function _M.new(_, nodes) 133 | local ids, points, npoints, newnodes = _precompute(nodes) 134 | 135 | local self = { 136 | nodes = newnodes, -- it's safer to copy one 137 | ids = ids, 138 | points = points, 139 | npoints = npoints, -- points number 140 | size = npoints, 141 | } 142 | return setmetatable(self, mt) 143 | end 144 | 145 | 146 | function _M.reinit(self, nodes) 147 | self.ids, self.points, self.npoints, self.nodes = _precompute(nodes) 148 | self.size = self.npoints 149 | end 150 | 151 | 152 | local function _delete(self, id) 153 | local nodes = self.nodes 154 | local ids = self.ids 155 | local old_weight = nodes[id] 156 | 157 | if not old_weight then 158 | return 159 | end 160 | 161 | local index = 1 162 | -- find the index: O(n) 163 | while ids[index] ~= id do 164 | index = index + 1 165 | end 166 | 167 | nodes[id] = nil 168 | ids[index] = nil 169 | 170 | clib.chash_point_delete(self.points, self.npoints, index) 171 | 172 | self.npoints = self.npoints - CONSISTENT_POINTS * old_weight 173 | end 174 | _M.delete = _delete 175 | 176 | 177 | local function _incr(self, id, weight) 178 | local weight = tonumber(weight) or 1 179 | local nodes = self.nodes 180 | local ids = self.ids 181 | local old_weight = nodes[id] 182 | 183 | local index = 1 184 | if old_weight then 185 | -- find the index: O(n) 186 | while ids[index] ~= id do 187 | index = index + 1 188 | end 189 | 190 | else 191 | old_weight = 0 192 | 193 | index = #ids + 1 194 | ids[index] = id 195 | end 196 | 197 | nodes[id] = old_weight + weight 198 | 199 | local new_points = self.points 200 | local new_npoints = self.npoints + weight * CONSISTENT_POINTS 201 | if self.size < new_npoints then 202 | new_points = ffi_new(chash_point_t, new_npoints) 203 | end 204 | 205 | local base_hash = bxor(crc32(tostring(id)), 0xffffffff) 206 | local rc = clib.chash_point_add(self.points, self.npoints, base_hash, 207 | old_weight * CONSISTENT_POINTS, 208 | weight * CONSISTENT_POINTS, 209 | index, new_points) 210 | 211 | if rc ~= CHASH_OK then 212 | error("no memory") 213 | end 214 | 215 | if self.size < new_npoints then 216 | self.size = new_npoints 217 | end 218 | 219 | self.points = new_points 220 | self.npoints = new_npoints 221 | end 222 | _M.incr = _incr 223 | 224 | 225 | local function _decr(self, id, weight) 226 | local weight = tonumber(weight) or 1 227 | local nodes = self.nodes 228 | local ids = self.ids 229 | local old_weight = nodes[id] 230 | 231 | if not old_weight then 232 | return 233 | end 234 | 235 | if old_weight <= weight then 236 | return _delete(self, id) 237 | end 238 | 239 | local index = 1 240 | -- find the index: O(n) 241 | while ids[index] ~= id do 242 | index = index + 1 243 | end 244 | 245 | local base_hash = bxor(crc32(tostring(id)), 0xffffffff) 246 | local from = (old_weight - weight) * CONSISTENT_POINTS 247 | local num = CONSISTENT_POINTS * weight 248 | 249 | local rc = clib.chash_point_reduce(self.points, self.npoints, base_hash, 250 | from, num, index) 251 | 252 | if rc ~= CHASH_OK then 253 | error("no memory") 254 | end 255 | 256 | nodes[id] = old_weight - weight 257 | self.npoints = self.npoints - CONSISTENT_POINTS * weight 258 | end 259 | _M.decr = _decr 260 | 261 | 262 | function _M.set(self, id, new_weight) 263 | local new_weight = tonumber(new_weight) or 0 264 | local old_weight = self.nodes[id] or 0 265 | 266 | if old_weight == new_weight then 267 | return true 268 | end 269 | 270 | if old_weight < new_weight then 271 | return _incr(self, id, new_weight - old_weight) 272 | end 273 | 274 | return _decr(self, id, old_weight - new_weight) 275 | end 276 | 277 | 278 | local function _find_id(points, npoints, hash) 279 | local step = pow32 / npoints 280 | local index = floor(hash / step) 281 | 282 | local max_index = npoints - 1 283 | 284 | -- it seems safer to do this 285 | if index > max_index then 286 | index = max_index 287 | end 288 | 289 | -- find the first points >= hash 290 | if points[index].hash >= hash then 291 | for i = index, 1, -1 do 292 | if points[i - 1].hash < hash then 293 | return points[i].id, i 294 | end 295 | end 296 | 297 | return points[0].id, 0 298 | end 299 | 300 | for i = index + 1, max_index do 301 | if hash <= points[i].hash then 302 | return points[i].id, i 303 | end 304 | end 305 | 306 | return points[0].id, 0 307 | end 308 | 309 | 310 | function _M.find(self, key) 311 | local hash = crc32(tostring(key)) 312 | 313 | local id, index = _find_id(self.points, self.npoints, hash) 314 | 315 | return self.ids[id], index 316 | end 317 | 318 | 319 | function _M.next(self, index) 320 | local new_index = (index + 1) % self.npoints 321 | local id = self.points[new_index].id 322 | 323 | return self.ids[id], new_index 324 | end 325 | 326 | 327 | return _M 328 | -------------------------------------------------------------------------------- /lib/resty/roundrobin.lua: -------------------------------------------------------------------------------- 1 | 2 | local pairs = pairs 3 | local next = next 4 | local tonumber = tonumber 5 | local setmetatable = setmetatable 6 | local math_random = math.random 7 | local error = error 8 | 9 | local utils = require "resty.balancer.utils" 10 | 11 | local copy = utils.copy 12 | local nkeys = utils.nkeys 13 | local new_tab = utils.new_tab 14 | 15 | local _M = {} 16 | local mt = { __index = _M } 17 | 18 | local _gcd 19 | _gcd = function (a, b) 20 | if b == 0 then 21 | return a 22 | end 23 | 24 | return _gcd(b, a % b) 25 | end 26 | 27 | 28 | local function get_gcd(nodes) 29 | local first_id, max_weight = next(nodes) 30 | if not first_id then 31 | return error("empty nodes") 32 | end 33 | 34 | local only_key = first_id 35 | local gcd = max_weight 36 | for id, weight in next, nodes, first_id do 37 | only_key = nil 38 | gcd = _gcd(gcd, weight) 39 | max_weight = weight > max_weight and weight or max_weight 40 | end 41 | 42 | return only_key, gcd, max_weight 43 | end 44 | 45 | local function get_random_node_id(nodes) 46 | local count = nkeys(nodes) 47 | 48 | local id = nil 49 | local random_index = math_random(count) 50 | 51 | for _ = 1, random_index do 52 | id = next(nodes, id) 53 | end 54 | 55 | return id 56 | end 57 | 58 | 59 | function _M.new(_, nodes) 60 | local newnodes = copy(nodes) 61 | local only_key, gcd, max_weight = get_gcd(newnodes) 62 | local last_id = get_random_node_id(nodes) 63 | 64 | local self = { 65 | nodes = newnodes, -- it's safer to copy one 66 | only_key = only_key, 67 | max_weight = max_weight, 68 | gcd = gcd, 69 | cw = max_weight, 70 | last_id = last_id, 71 | } 72 | return setmetatable(self, mt) 73 | end 74 | 75 | 76 | function _M.reinit(self, nodes) 77 | local newnodes = copy(nodes) 78 | self.only_key, self.gcd, self.max_weight = get_gcd(newnodes) 79 | 80 | self.nodes = newnodes 81 | self.last_id = get_random_node_id(nodes) 82 | self.cw = self.max_weight 83 | end 84 | 85 | 86 | local function _delete(self, id) 87 | local nodes = self.nodes 88 | 89 | nodes[id] = nil 90 | 91 | self.only_key, self.gcd, self.max_weight = get_gcd(nodes) 92 | 93 | if id == self.last_id then 94 | self.last_id = nil 95 | end 96 | 97 | if self.cw > self.max_weight then 98 | self.cw = self.max_weight 99 | end 100 | end 101 | _M.delete = _delete 102 | 103 | 104 | local function _decr(self, id, weight) 105 | local weight = tonumber(weight) or 1 106 | local nodes = self.nodes 107 | 108 | local old_weight = nodes[id] 109 | if not old_weight then 110 | return 111 | end 112 | 113 | if old_weight <= weight then 114 | return _delete(self, id) 115 | end 116 | 117 | nodes[id] = old_weight - weight 118 | 119 | self.only_key, self.gcd, self.max_weight = get_gcd(nodes) 120 | 121 | if self.cw > self.max_weight then 122 | self.cw = self.max_weight 123 | end 124 | end 125 | _M.decr = _decr 126 | 127 | 128 | local function _incr(self, id, weight) 129 | local weight = tonumber(weight) or 1 130 | local nodes = self.nodes 131 | 132 | nodes[id] = (nodes[id] or 0) + weight 133 | 134 | self.only_key, self.gcd, self.max_weight = get_gcd(nodes) 135 | end 136 | _M.incr = _incr 137 | 138 | 139 | 140 | function _M.set(self, id, new_weight) 141 | local new_weight = tonumber(new_weight) or 0 142 | local old_weight = self.nodes[id] or 0 143 | 144 | if old_weight == new_weight then 145 | return 146 | end 147 | 148 | if old_weight < new_weight then 149 | return _incr(self, id, new_weight - old_weight) 150 | end 151 | 152 | return _decr(self, id, old_weight - new_weight) 153 | end 154 | 155 | 156 | local function find(self) 157 | local only_key = self.only_key 158 | if only_key then 159 | return only_key 160 | end 161 | 162 | local nodes = self.nodes 163 | local last_id, cw, weight = self.last_id, self.cw 164 | 165 | while true do 166 | while true do 167 | last_id, weight = next(nodes, last_id) 168 | if not last_id then 169 | break 170 | end 171 | 172 | if weight >= cw then 173 | self.cw = cw 174 | self.last_id = last_id 175 | return last_id 176 | end 177 | end 178 | 179 | cw = cw - self.gcd 180 | if cw <= 0 then 181 | cw = self.max_weight 182 | end 183 | end 184 | end 185 | _M.find = find 186 | _M.next = find 187 | 188 | 189 | return _M 190 | -------------------------------------------------------------------------------- /lib/resty/swrr.lua: -------------------------------------------------------------------------------- 1 | local pairs = pairs 2 | local next = next 3 | local tonumber = tonumber 4 | local setmetatable = setmetatable 5 | local math_random = math.random 6 | local error = error 7 | 8 | local utils = require "resty.balancer.utils" 9 | 10 | local copy = utils.copy 11 | local nkeys = utils.nkeys 12 | local new_tab = utils.new_tab 13 | 14 | local _M = {} 15 | local mt = { __index = _M } 16 | 17 | 18 | local function new_current_weights(nodes) 19 | local current_weights = new_tab(0, nkeys(nodes)) 20 | for id, _ in pairs(nodes) do 21 | current_weights[id] = 0 22 | end 23 | return current_weights 24 | end 25 | 26 | 27 | local function random_start(self) 28 | local count = nkeys(self.nodes) 29 | local random_times = math_random(count) 30 | 31 | for _ = 1, random_times do 32 | self:next() 33 | end 34 | end 35 | 36 | 37 | function _M.new(_, nodes) 38 | local newnodes = copy(nodes) 39 | local current_weights = new_current_weights(nodes) 40 | 41 | local self = { 42 | nodes = newnodes, -- it's safer to copy one 43 | current_weights = current_weights, 44 | } 45 | self = setmetatable(self, mt) 46 | random_start(self) 47 | return self 48 | end 49 | 50 | 51 | function _M.reinit(self, nodes) 52 | local newnodes = copy(nodes) 53 | local current_weights = new_current_weights(newnodes) 54 | 55 | self.nodes = newnodes 56 | self.current_weights = current_weights 57 | random_start(self) 58 | end 59 | 60 | 61 | local function _delete(self, id) 62 | local nodes = self.nodes 63 | local current_weights = self.current_weights 64 | 65 | nodes[id] = nil 66 | current_weights[id] = nil 67 | end 68 | _M.delete = _delete 69 | 70 | 71 | local function _decr(self, id, weight) 72 | local weight = tonumber(weight) or 1 73 | local nodes = self.nodes 74 | 75 | local old_weight = nodes[id] 76 | 77 | if not old_weight then 78 | return 79 | end 80 | 81 | if old_weight <= weight then 82 | return _delete(self, id) 83 | end 84 | 85 | nodes[id] = old_weight - weight 86 | end 87 | _M.decr = _decr 88 | 89 | 90 | local function _incr(self, id, weight) 91 | local weight = tonumber(weight) or 1 92 | local nodes = self.nodes 93 | 94 | nodes[id] = (nodes[id] or 0) + weight 95 | end 96 | _M.incr = _incr 97 | 98 | 99 | function _M.set(self, id, new_weight) 100 | local new_weight = tonumber(new_weight) or 0 101 | local old_weight = self.nodes[id] or 0 102 | 103 | if old_weight == new_weight then 104 | return 105 | end 106 | 107 | if old_weight < new_weight then 108 | return _incr(self, id, new_weight - old_weight) 109 | end 110 | 111 | return _decr(self, id, old_weight - new_weight) 112 | end 113 | 114 | 115 | local function find(self) 116 | local nodes = self.nodes 117 | local current_weights = self.current_weights 118 | 119 | local best_id = nil 120 | local best_current_weight = 0 121 | local total = 0 122 | 123 | for id, weight in pairs(nodes) do 124 | local current_weight = current_weights[id] 125 | total = total + weight 126 | current_weight = current_weight + weight 127 | current_weights[id] = current_weight 128 | 129 | if best_id == nil or best_current_weight < current_weight then 130 | best_id = id 131 | best_current_weight = current_weight 132 | end 133 | end 134 | 135 | if best_id ~= nil then 136 | current_weights[best_id] = best_current_weight - total 137 | end 138 | 139 | return best_id 140 | end 141 | _M.find = find 142 | _M.next = find 143 | 144 | 145 | return _M 146 | -------------------------------------------------------------------------------- /t/bench-chash.lua: -------------------------------------------------------------------------------- 1 | require "resty.core" 2 | require "jit.opt".start("minstitch=2", "maxtrace=4000", 3 | "maxrecord=8000", "sizemcode=64", 4 | "maxmcode=4000", "maxirconst=1000") 5 | 6 | local local_dir = arg[1] 7 | 8 | -- ngx.say("local dir: ", local_dir) 9 | 10 | package.path = local_dir .. "/lib/?.lua;" .. package.path 11 | package.cpath = local_dir .. "/?.so;" .. package.cpath 12 | 13 | local base_time 14 | 15 | -- should run typ = nil first 16 | local function bench(num, name, func, typ, ...) 17 | ngx.update_time() 18 | local start = ngx.now() 19 | 20 | for i = 1, num do 21 | func(...) 22 | end 23 | 24 | ngx.update_time() 25 | local elasped = ngx.now() - start 26 | 27 | if typ then 28 | elasped = elasped - base_time 29 | end 30 | 31 | ngx.say(name) 32 | ngx.say(num, " times") 33 | ngx.say("elasped: ", elasped) 34 | ngx.say("") 35 | 36 | if not typ then 37 | base_time = elasped 38 | end 39 | end 40 | 41 | 42 | local resty_chash = require "resty.chash" 43 | 44 | local servers = { 45 | ["server1"] = 10, 46 | ["server2"] = 2, 47 | ["server3"] = 1, 48 | } 49 | 50 | local servers2 = { 51 | ["server1"] = 100, 52 | ["server2"] = 20, 53 | ["server3"] = 10, 54 | } 55 | 56 | local servers3 = { 57 | ["server0"] = 1, 58 | ["server1"] = 1, 59 | ["server2"] = 1, 60 | ["server3"] = 1, 61 | ["server4"] = 1, 62 | ["server5"] = 1, 63 | ["server6"] = 1, 64 | ["server7"] = 1, 65 | ["server8"] = 1, 66 | ["server9"] = 1, 67 | ["server10"] = 1, 68 | ["server11"] = 1, 69 | ["server12"] = 1, 70 | } 71 | 72 | local chash = resty_chash:new(servers) 73 | 74 | local function gen_func(typ) 75 | local i = 0 76 | 77 | if typ == 0 then 78 | return function () 79 | i = i + 1 80 | 81 | resty_chash:new(servers) 82 | end 83 | end 84 | 85 | if typ == 1 then 86 | return function () 87 | i = i + 1 88 | 89 | local servers = { 90 | ["server1" .. i] = 10, 91 | ["server2" .. i] = 2, 92 | ["server3" .. i] = 1, 93 | } 94 | local chash = resty_chash:new(servers) 95 | end 96 | end 97 | 98 | if typ == 2 then 99 | return function () 100 | i = i + 1 101 | 102 | local servers = { 103 | ["server1" .. i] = 10, 104 | ["server2" .. i] = 2, 105 | ["server3" .. i] = 1, 106 | } 107 | local chash = resty_chash:new(servers) 108 | chash:incr("server3" .. i) 109 | end, typ 110 | end 111 | 112 | if typ == 3 then 113 | return function () 114 | i = i + 1 115 | 116 | local servers = { 117 | ["server1" .. i] = 10, 118 | ["server2" .. i] = 2, 119 | ["server3" .. i] = 1, 120 | } 121 | local chash = resty_chash:new(servers) 122 | chash:incr("server1" .. i) 123 | end, typ 124 | end 125 | 126 | if typ == 4 then 127 | return function () 128 | i = i + 1 129 | 130 | local servers = { 131 | ["server1" .. i] = 10, 132 | ["server2" .. i] = 2, 133 | ["server3" .. i] = 1, 134 | } 135 | local chash = resty_chash:new(servers) 136 | chash:decr("server1" .. i) 137 | end, typ 138 | end 139 | 140 | if typ == 5 then 141 | return function () 142 | i = i + 1 143 | 144 | local servers = { 145 | ["server1" .. i] = 10, 146 | ["server2" .. i] = 2, 147 | ["server3" .. i] = 1, 148 | } 149 | local chash = resty_chash:new(servers) 150 | chash:delete("server3" .. i) 151 | end, typ 152 | end 153 | 154 | if typ == 6 then 155 | return function () 156 | i = i + 1 157 | 158 | local servers = { 159 | ["server1" .. i] = 10, 160 | ["server2" .. i] = 2, 161 | ["server3" .. i] = 1, 162 | } 163 | local chash = resty_chash:new(servers) 164 | chash:delete("server1" .. i) 165 | end, typ 166 | end 167 | 168 | if typ == 7 then 169 | return function () 170 | i = i + 1 171 | 172 | local servers = { 173 | ["server1" .. i] = 10, 174 | ["server2" .. i] = 2, 175 | ["server3" .. i] = 1, 176 | } 177 | local chash = resty_chash:new(servers) 178 | chash:set("server1" .. i, 9) 179 | end, typ 180 | end 181 | 182 | if typ == 8 then 183 | return function () 184 | i = i + 1 185 | 186 | local servers = { 187 | ["server1" .. i] = 10, 188 | ["server2" .. i] = 2, 189 | ["server3" .. i] = 1, 190 | } 191 | local chash = resty_chash:new(servers) 192 | chash:set("server1" .. i, 8) 193 | end, typ 194 | end 195 | 196 | if typ == 9 then 197 | return function () 198 | i = i + 1 199 | 200 | local servers = { 201 | ["server1" .. i] = 10, 202 | ["server2" .. i] = 2, 203 | ["server3" .. i] = 1, 204 | } 205 | local chash = resty_chash:new(servers) 206 | chash:set("server1" .. i, 1) 207 | end, typ 208 | end 209 | 210 | if typ == 100 then 211 | return function () 212 | i = i + 1 213 | end 214 | end 215 | 216 | if typ == 101 then 217 | return function () 218 | i = i + 1 219 | 220 | chash:find(i) 221 | i = i + 1 222 | end, typ 223 | end 224 | 225 | if typ == 102 then 226 | return function () 227 | i = i + 1 228 | 229 | chash:simple_find(i) 230 | end, typ 231 | end 232 | end 233 | 234 | bench(10 * 1000, "chash new servers", resty_chash.new, nil, nil, servers) 235 | bench(1 * 1000, "chash new servers2", resty_chash.new, nil, nil, servers2) 236 | bench(10 * 1000, "chash new servers3", resty_chash.new, nil, nil, servers3) 237 | bench(10 * 1000, "new in func", gen_func(0)) 238 | bench(10 * 1000, "new dynamic", gen_func(1)) 239 | bench(10 * 1000, "incr server3", gen_func(2)) 240 | bench(10 * 1000, "incr server1", gen_func(3)) 241 | bench(10 * 1000, "decr server1", gen_func(4)) 242 | bench(10 * 1000, "delete server3", gen_func(5)) 243 | bench(10 * 1000, "delete server1", gen_func(6)) 244 | bench(10 * 1000, "set server1 9", gen_func(7)) 245 | bench(10 * 1000, "set server1 8", gen_func(8)) 246 | bench(10 * 1000, "set server1 1", gen_func(9)) 247 | 248 | bench(1000 * 1000, "base for find", gen_func(100)) 249 | bench(1000 * 1000, "find", gen_func(101)) 250 | -------------------------------------------------------------------------------- /t/bench-roundrobin.lua: -------------------------------------------------------------------------------- 1 | require "resty.core" 2 | require "jit.opt".start("minstitch=2", "maxtrace=4000", 3 | "maxrecord=8000", "sizemcode=64", 4 | "maxmcode=4000", "maxirconst=1000") 5 | 6 | local local_dir = arg[1] 7 | 8 | -- ngx.say("local dir: ", local_dir) 9 | 10 | package.path = local_dir .. "/lib/?.lua;" .. package.path 11 | package.cpath = local_dir .. "/?.so;" .. package.cpath 12 | 13 | local base_time 14 | 15 | -- should run typ = nil first 16 | local function bench(num, name, func, typ, ...) 17 | ngx.update_time() 18 | local start = ngx.now() 19 | 20 | for i = 1, num do 21 | func(...) 22 | end 23 | 24 | ngx.update_time() 25 | local elasped = ngx.now() - start 26 | 27 | if typ then 28 | elasped = elasped - base_time 29 | end 30 | 31 | ngx.say(name) 32 | ngx.say(num, " times") 33 | ngx.say("elasped: ", elasped) 34 | ngx.say("") 35 | 36 | if not typ then 37 | base_time = elasped 38 | end 39 | end 40 | 41 | 42 | local resty_rr = require "resty.roundrobin" 43 | 44 | local servers = { 45 | ["server1"] = 10, 46 | ["server2"] = 2, 47 | ["server3"] = 1, 48 | } 49 | 50 | local servers2 = { 51 | ["server1"] = 100, 52 | ["server2"] = 20, 53 | ["server3"] = 10, 54 | } 55 | 56 | local servers3 = { 57 | ["server0"] = 1, 58 | ["server1"] = 1, 59 | ["server2"] = 1, 60 | ["server3"] = 1, 61 | ["server4"] = 1, 62 | ["server5"] = 1, 63 | ["server6"] = 1, 64 | ["server7"] = 1, 65 | ["server8"] = 1, 66 | ["server9"] = 1, 67 | ["server10"] = 1, 68 | ["server11"] = 1, 69 | ["server12"] = 1, 70 | } 71 | 72 | local rr = resty_rr:new(servers) 73 | 74 | local function gen_func(typ) 75 | local i = 0 76 | 77 | if typ == 0 then 78 | return function () 79 | i = i + 1 80 | 81 | resty_rr:new(servers) 82 | end 83 | end 84 | 85 | if typ == 1 then 86 | return function () 87 | i = i + 1 88 | 89 | local servers = { 90 | ["server1" .. i] = 10, 91 | ["server2" .. i] = 2, 92 | ["server3" .. i] = 1, 93 | } 94 | local rr = resty_rr:new(servers) 95 | end 96 | end 97 | 98 | if typ == 2 then 99 | return function () 100 | i = i + 1 101 | 102 | local servers = { 103 | ["server1" .. i] = 10, 104 | ["server2" .. i] = 2, 105 | ["server3" .. i] = 1, 106 | } 107 | local rr = resty_rr:new(servers) 108 | rr:incr("server3" .. i) 109 | end, typ 110 | end 111 | 112 | if typ == 100 then 113 | return function () 114 | i = i + 1 115 | end 116 | end 117 | 118 | if typ == 101 then 119 | return function () 120 | i = i + 1 121 | 122 | rr:find(i) 123 | i = i + 1 124 | end, typ 125 | end 126 | end 127 | 128 | bench(10 * 1000, "rr new servers", resty_rr.new, nil, nil, servers) 129 | bench(1 * 1000, "rr new servers2", resty_rr.new, nil, nil, servers2) 130 | bench(10 * 1000, "rr new servers3", resty_rr.new, nil, nil, servers3) 131 | bench(10 * 1000, "new in func", gen_func(0)) 132 | bench(10 * 1000, "new dynamic", gen_func(1)) 133 | bench(10 * 1000, "incr server3", gen_func(2)) 134 | 135 | bench(1000 * 1000, "base for find", gen_func(100)) 136 | 137 | bench(1000 * 1000, "find from 3 servers", gen_func(101)) 138 | rr:delete("server2") 139 | bench(1000 * 1000, "find from 2 servers", gen_func(101)) 140 | rr:delete("server3") 141 | bench(1000 * 1000, "find from 1 server", gen_func(101)) 142 | -------------------------------------------------------------------------------- /t/chash.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | $ENV{TEST_NGINX_CWD} = $pwd; 13 | 14 | our $HttpConfig = qq{ 15 | lua_package_path "$pwd/lib/?.lua;;"; 16 | lua_package_cpath "$pwd/?.so;;"; 17 | }; 18 | 19 | no_long_string(); 20 | #no_diff(); 21 | 22 | run_tests(); 23 | 24 | __DATA__ 25 | 26 | === TEST 1: find 27 | --- http_config eval: $::HttpConfig 28 | --- config 29 | location /t { 30 | content_by_lua_block { 31 | local resty_chash = require "resty.chash" 32 | 33 | local servers = { 34 | ["server1"] = 10, 35 | ["server2"] = 2, 36 | ["server3"] = 1, 37 | } 38 | 39 | local chash = resty_chash:new(servers) 40 | 41 | local res = {} 42 | for i = 1, 100 * 1000 do 43 | local id = chash:find(i) 44 | if res[id] then 45 | res[id] = res[id] + 1 46 | else 47 | res[id] = 1 48 | end 49 | end 50 | 51 | for i=1, 3 do 52 | local id = "server"..i 53 | ngx.say(id..": ", res[id]) 54 | end 55 | 56 | ngx.say("points number: ", chash.npoints) 57 | } 58 | } 59 | --- request 60 | GET /t 61 | --- response_body 62 | server1: 77075 63 | server2: 14743 64 | server3: 8182 65 | points number: 2080 66 | --- no_error_log 67 | [error] 68 | --- timeout: 30 69 | 70 | 71 | 72 | === TEST 2: compare with nginx chash 73 | --- http_config 74 | lua_package_path "$TEST_NGINX_CWD/lib/?.lua;;"; 75 | lua_package_cpath "$TEST_NGINX_CWD/?.so;;"; 76 | 77 | server { 78 | listen 1985; 79 | listen 1986; 80 | listen 1987; 81 | server_name 'localhost'; 82 | 83 | location / { 84 | content_by_lua_block { 85 | ngx.say(ngx.var.server_port) 86 | } 87 | } 88 | } 89 | 90 | init_by_lua_block { 91 | local resty_chash = require "resty.chash" 92 | 93 | local server_list = { 94 | ["127.0.0.1:1985"] = 2, 95 | ["127.0.0.1:1986"] = 2, 96 | ["127.0.0.1:1987"] = 1, 97 | } 98 | 99 | local str_null = string.char(0) 100 | 101 | local servers, nodes = {}, {} 102 | for serv, weight in pairs(server_list) do 103 | local id = string.gsub(serv, ":", str_null) 104 | 105 | servers[id] = serv 106 | nodes[id] = weight 107 | end 108 | 109 | local chash_up = resty_chash:new(nodes) 110 | 111 | package.loaded.my_chash_up = chash_up 112 | package.loaded.my_servers = servers 113 | } 114 | 115 | upstream backend_lua { 116 | server 0.0.0.1; 117 | balancer_by_lua_block { 118 | print("hello from balancer by lua!") 119 | local b = require "ngx.balancer" 120 | 121 | local chash_up = package.loaded.my_chash_up 122 | local servers = package.loaded.my_servers 123 | 124 | local id = chash_up:find(ngx.var.arg_key) 125 | local server = servers[id] 126 | 127 | assert(b.set_current_peer(server)) 128 | } 129 | } 130 | 131 | upstream backend_ngx { 132 | hash $arg_key consistent; 133 | 134 | server 127.0.0.1:1985 weight=2; 135 | server 127.0.0.1:1986 weight=2; 136 | server 127.0.0.1:1987 weight=1; 137 | } 138 | --- config 139 | location = /lua { 140 | proxy_pass http://backend_lua; 141 | } 142 | location = /ngx { 143 | proxy_next_upstream_tries 0; 144 | proxy_next_upstream off; 145 | 146 | proxy_pass http://backend_ngx; 147 | } 148 | location = /main { 149 | content_by_lua_block { 150 | math.randomseed(ngx.now()) 151 | local start = math.random(1, 1000000) 152 | 153 | -- ngx.log(ngx.ERR, start) 154 | for i = start + 1, start + 100 do 155 | local res1 = ngx.location.capture("/lua?key=" .. i) 156 | local res2 = ngx.location.capture("/ngx?key=" .. i) 157 | 158 | -- ngx.log(ngx.ERR, res1.body) 159 | if res1.body ~= res2.body then 160 | ngx.log(ngx.ERR, "not matched upstream, key:", i) 161 | end 162 | end 163 | ngx.say("ok") 164 | } 165 | } 166 | --- request 167 | GET /main 168 | --- response_body 169 | ok 170 | --- error_code: 200 171 | --- no_error_log 172 | [error] 173 | --- timeout: 10 174 | 175 | 176 | 177 | === TEST 3: next 178 | --- http_config eval: $::HttpConfig 179 | --- config 180 | location /t { 181 | content_by_lua_block { 182 | local resty_chash = require "resty.chash" 183 | 184 | local servers = { 185 | ["server1"] = 2, 186 | ["server2"] = 2, 187 | ["server3"] = 1, 188 | } 189 | 190 | local chash = resty_chash:new(servers) 191 | 192 | local id, idx = chash:find("foo") 193 | ngx.say(id, ", ", idx) 194 | 195 | for i = 1, 100 do 196 | id, idx = chash:next(idx) 197 | end 198 | ngx.say(id, ", ", idx) 199 | } 200 | } 201 | --- request 202 | GET /t 203 | --- response_body 204 | server1, 434 205 | server2, 534 206 | --- no_error_log 207 | [error] 208 | --- timeout: 10 209 | 210 | 211 | 212 | === TEST 4: up, decr 213 | --- http_config eval: $::HttpConfig 214 | --- config 215 | location /t { 216 | content_by_lua_block { 217 | local resty_chash = require "resty.chash" 218 | 219 | local servers = { 220 | ["server1"] = 7, 221 | ["server2"] = 2, 222 | ["server3"] = 1, 223 | } 224 | 225 | local chash = resty_chash:new(servers) 226 | 227 | local num = 100 * 1000 228 | 229 | local res1 = {} 230 | for i = 1, num do 231 | local id = chash:find(i) 232 | 233 | res1[i] = id 234 | end 235 | 236 | chash:incr("server1") 237 | 238 | local res2 = {} 239 | for i = 1, num do 240 | local id = chash:find(i) 241 | 242 | res2[i] = id 243 | end 244 | 245 | local same, diff = 0, 0 246 | for i = 1, num do 247 | if res1[i] == res2[i] then 248 | same = same + 1 249 | else 250 | diff = diff + 1 251 | end 252 | end 253 | 254 | ngx.say("same: ", same) 255 | ngx.say("diff: ", diff) 256 | 257 | chash:decr("server3") 258 | 259 | local res3 = {} 260 | for i = 1, num do 261 | local id = chash:find(i) 262 | 263 | res3[i] = id 264 | end 265 | 266 | local same, diff = 0, 0 267 | for i = 1, num do 268 | if res3[i] == res2[i] then 269 | same = same + 1 270 | else 271 | diff = diff + 1 272 | end 273 | end 274 | 275 | ngx.say("same: ", same) 276 | ngx.say("diff: ", diff) 277 | } 278 | } 279 | --- request 280 | GET /t 281 | --- response_body 282 | same: 97606 283 | diff: 2394 284 | same: 90255 285 | diff: 9745 286 | --- no_error_log 287 | [error] 288 | --- timeout: 30 289 | 290 | 291 | 292 | === TEST 5: reinit 293 | --- http_config eval: $::HttpConfig 294 | --- config 295 | location /t { 296 | content_by_lua_block { 297 | local resty_chash = require "resty.chash" 298 | 299 | local servers = { 300 | ["server1"] = 10, 301 | ["server2"] = 2, 302 | ["server3"] = 1, 303 | } 304 | 305 | local chash = resty_chash:new(servers) 306 | 307 | local success = true 308 | local count = 0 309 | 310 | for id, weight in pairs(chash.nodes) do 311 | count = count + 1 312 | if servers[id] ~= weight then 313 | success = false 314 | end 315 | end 316 | ngx.say("count: ", count) 317 | ngx.say("success: ",success) 318 | 319 | ngx.say("points number: ", chash.npoints) 320 | ngx.say("size: ", chash.size) 321 | 322 | ngx.say("reinit") 323 | 324 | local new_servers = { 325 | ["server4"] = 1, 326 | ["server5"] = 2, 327 | } 328 | chash:reinit(new_servers) 329 | 330 | count = 0 331 | for id, weight in pairs(chash.nodes) do 332 | count = count + 1 333 | if new_servers[id] ~= weight then 334 | success = false 335 | end 336 | end 337 | ngx.say("count: ", count) 338 | ngx.say("success: ",success) 339 | 340 | ngx.say("points number: ", chash.npoints) 341 | ngx.say("size: ", chash.size) 342 | } 343 | } 344 | --- request 345 | GET /t 346 | --- response_body 347 | count: 3 348 | success: true 349 | points number: 2080 350 | size: 2080 351 | reinit 352 | count: 2 353 | success: true 354 | points number: 480 355 | size: 480 356 | --- no_error_log 357 | [error] 358 | --- timeout: 30 359 | 360 | 361 | 362 | === TEST 6: random key fuzzer 363 | --- http_config eval: $::HttpConfig 364 | --- config 365 | location /t { 366 | content_by_lua_block { 367 | math.randomseed(ngx.now()) 368 | 369 | local ffi = require "ffi" 370 | local resty_chash = require "resty.chash" 371 | 372 | local function random_string() 373 | local len = math.random(10, 100) 374 | local buf = ffi.new("char [?]", len) 375 | for i = 0, len - 1 do 376 | buf[i] = math.random(0, 255) 377 | end 378 | 379 | return ffi.string(buf, len) 380 | end 381 | 382 | for i = 1, 30 do 383 | local servers = {} 384 | 385 | local len = math.random(1, 100) 386 | for j = 1, len do 387 | local key = random_string() 388 | servers[key] = math.random(1, 100) 389 | end 390 | 391 | local chash = resty_chash:new(servers) 392 | end 393 | 394 | ngx.say("done") 395 | } 396 | } 397 | --- request 398 | GET /t 399 | --- response_body 400 | done 401 | --- no_error_log 402 | [error] 403 | --- timeout: 30 404 | -------------------------------------------------------------------------------- /t/roundrobin.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | $ENV{TEST_NGINX_CWD} = $pwd; 13 | 14 | our $HttpConfig = qq{ 15 | lua_package_path "$pwd/lib/?.lua;;"; 16 | lua_package_cpath "$pwd/?.so;;"; 17 | }; 18 | 19 | no_long_string(); 20 | #no_diff(); 21 | 22 | run_tests(); 23 | 24 | __DATA__ 25 | 26 | === TEST 1: sanity 27 | --- http_config eval: $::HttpConfig 28 | --- config 29 | location /t { 30 | content_by_lua_block { 31 | math.randomseed(75098) 32 | 33 | local roundrobin = require "resty.roundrobin" 34 | 35 | local servers = { 36 | ["server1"] = 8, 37 | ["server2"] = 4, 38 | ["server3"] = 2, 39 | } 40 | 41 | local rr = roundrobin:new(servers) 42 | 43 | ngx.say("gcd: ", rr.gcd) 44 | 45 | for i = 1, 14 do 46 | local id = rr:find() 47 | if type(id) ~= "string" or not servers[id] then 48 | return ngx.say("fail") 49 | end 50 | end 51 | 52 | ngx.say("success") 53 | } 54 | } 55 | --- request 56 | GET /t 57 | --- response_body 58 | gcd: 2 59 | success 60 | --- no_error_log 61 | [error] 62 | 63 | 64 | 65 | === TEST 2: find count 66 | --- http_config eval: $::HttpConfig 67 | --- config 68 | location /t { 69 | content_by_lua_block { 70 | math.randomseed(75098) 71 | 72 | local roundrobin = require "resty.roundrobin" 73 | 74 | local servers = { 75 | ["server1"] = 6, 76 | ["server2"] = 3, 77 | ["server3"] = 1, 78 | } 79 | 80 | local rr = roundrobin:new(servers) 81 | 82 | local res = {} 83 | for i = 1, 100 * 1000 do 84 | local id = rr:find() 85 | 86 | if res[id] then 87 | res[id] = res[id] + 1 88 | else 89 | res[id] = 1 90 | end 91 | end 92 | 93 | local keys = {} 94 | for id, num in pairs(res) do 95 | keys[#keys + 1] = id 96 | end 97 | 98 | if #keys ~= 3 then 99 | ngx.exit(400) 100 | end 101 | 102 | ngx.say("server1: ", res['server1']) 103 | ngx.say("server2: ", res['server2']) 104 | ngx.say("server3: ", res['server3']) 105 | } 106 | } 107 | --- request 108 | GET /t 109 | --- response_body 110 | server1: 60000 111 | server2: 30000 112 | server3: 10000 113 | --- no_error_log 114 | [error] 115 | 116 | 117 | 118 | === TEST 3: random start 119 | --- http_config eval: $::HttpConfig 120 | --- config 121 | location /t { 122 | content_by_lua_block { 123 | math.randomseed(9975098) 124 | 125 | local roundrobin = require "resty.roundrobin" 126 | 127 | local servers = { 128 | ["server1"] = 1, 129 | ["server2"] = 1, 130 | ["server3"] = 1, 131 | ["server4"] = 1, 132 | } 133 | 134 | local rr = roundrobin:new(servers, true) 135 | local id = rr:find() 136 | 137 | local rr2 = roundrobin:new(servers, true) 138 | local id2 = rr2:find() 139 | ngx.log(ngx.INFO, "id: ", id, " id2: ", id2) 140 | ngx.say(id == id2) 141 | } 142 | } 143 | --- request 144 | GET /t 145 | --- response_body 146 | false 147 | --- no_error_log 148 | [error] 149 | 150 | 151 | 152 | === TEST 4: weight is "0" 153 | --- http_config eval: $::HttpConfig 154 | --- config 155 | location /t { 156 | content_by_lua_block { 157 | math.randomseed(9975098) 158 | 159 | local roundrobin = require "resty.roundrobin" 160 | 161 | local servers = { 162 | ["server1"] = "0", 163 | ["server2"] = "1", 164 | ["server3"] = "0", 165 | ["server4"] = "0", 166 | } 167 | 168 | local rr = roundrobin:new(servers, true) 169 | local id = rr:find() 170 | 171 | ngx.say("id: ", id) 172 | } 173 | } 174 | --- request 175 | GET /t 176 | --- response_body 177 | id: server2 178 | --- no_error_log 179 | [error] 180 | 181 | 182 | 183 | === TEST 5: all weights are 0, behavior like weights are 1. 184 | It's not recommends to use 0, this test just make sure it won't be worse, like crash. 185 | --- http_config eval: $::HttpConfig 186 | --- config 187 | location /t { 188 | content_by_lua_block { 189 | math.randomseed(9975098) 190 | 191 | local roundrobin = require "resty.roundrobin" 192 | 193 | local servers = { 194 | ["server1"] = 0, 195 | ["server2"] = 0, 196 | ["server3"] = 0, 197 | ["server4"] = 0, 198 | } 199 | 200 | local rr = roundrobin:new(servers, true) 201 | 202 | for i = 1, 4 do 203 | local id = rr:find() 204 | end 205 | 206 | ngx.say("ok") 207 | } 208 | } 209 | --- request 210 | GET /t 211 | --- response_body 212 | ok 213 | --- no_error_log 214 | [error] 215 | -------------------------------------------------------------------------------- /t/swrr.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | $ENV{TEST_NGINX_CWD} = $pwd; 13 | 14 | our $HttpConfig = qq{ 15 | lua_package_path "$pwd/lib/?.lua;;"; 16 | lua_package_cpath "$pwd/?.so;;"; 17 | }; 18 | 19 | no_long_string(); 20 | #no_diff(); 21 | 22 | run_tests(); 23 | 24 | __DATA__ 25 | 26 | === TEST 1: sanity 27 | --- http_config eval: $::HttpConfig 28 | --- config 29 | location /t { 30 | content_by_lua_block { 31 | math.randomseed(75098) 32 | 33 | local swrr = require "resty.swrr" 34 | 35 | local servers = { 36 | ["server1"] = 8, 37 | ["server2"] = 4, 38 | ["server3"] = 2, 39 | } 40 | 41 | local rr = swrr:new(servers) 42 | 43 | for i = 1, 14 do 44 | local id = rr:find() 45 | if type(id) ~= "string" or not servers[id] then 46 | return ngx.say("fail") 47 | end 48 | end 49 | 50 | ngx.say("success") 51 | } 52 | } 53 | --- request 54 | GET /t 55 | --- response_body 56 | success 57 | --- no_error_log 58 | [error] 59 | 60 | 61 | 62 | === TEST 2: find count 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | location /t { 66 | content_by_lua_block { 67 | math.randomseed(75098) 68 | 69 | local swrr = require "resty.swrr" 70 | 71 | local servers = { 72 | ["server1"] = 6, 73 | ["server2"] = 3, 74 | ["server3"] = 1, 75 | } 76 | 77 | local rr = swrr:new(servers) 78 | 79 | local res = {} 80 | for i = 1, 100 * 1000 do 81 | local id = rr:find() 82 | 83 | if res[id] then 84 | res[id] = res[id] + 1 85 | else 86 | res[id] = 1 87 | end 88 | end 89 | 90 | local keys = {} 91 | for id, num in pairs(res) do 92 | keys[#keys + 1] = id 93 | end 94 | 95 | if #keys ~= 3 then 96 | ngx.exit(400) 97 | end 98 | 99 | ngx.say("server1: ", res['server1']) 100 | ngx.say("server2: ", res['server2']) 101 | ngx.say("server3: ", res['server3']) 102 | } 103 | } 104 | --- request 105 | GET /t 106 | --- response_body 107 | server1: 60000 108 | server2: 30000 109 | server3: 10000 110 | --- no_error_log 111 | [error] 112 | 113 | 114 | 115 | === TEST 3: random start 116 | --- http_config eval: $::HttpConfig 117 | --- config 118 | location /t { 119 | content_by_lua_block { 120 | math.randomseed(9975098) 121 | 122 | local swrr = require "resty.swrr" 123 | 124 | local servers = { 125 | ["server1"] = 1, 126 | ["server2"] = 1, 127 | ["server3"] = 1, 128 | ["server4"] = 1, 129 | } 130 | 131 | local rr = swrr:new(servers, true) 132 | local id = rr:find() 133 | 134 | local rr2 = swrr:new(servers, true) 135 | local id2 = rr2:find() 136 | ngx.log(ngx.INFO, "id: ", id, " id2: ", id2) 137 | ngx.say(id == id2) 138 | } 139 | } 140 | --- request 141 | GET /t 142 | --- response_body 143 | false 144 | --- no_error_log 145 | [error] 146 | 147 | 148 | 149 | === TEST 4: weight is "0" 150 | --- http_config eval: $::HttpConfig 151 | --- config 152 | location /t { 153 | content_by_lua_block { 154 | math.randomseed(9975098) 155 | 156 | local swrr = require "resty.swrr" 157 | 158 | local servers = { 159 | ["server1"] = "0", 160 | ["server2"] = "1", 161 | ["server3"] = "0", 162 | ["server4"] = "0", 163 | } 164 | 165 | local rr = swrr:new(servers, true) 166 | local id = rr:find() 167 | 168 | ngx.say("id: ", id) 169 | } 170 | } 171 | --- request 172 | GET /t 173 | --- response_body 174 | id: server2 175 | --- no_error_log 176 | [error] 177 | 178 | 179 | 180 | === TEST 5: all weights are 0, behavior like weights are 1. 181 | It's not recommends to use 0, this test just make sure it won't be worse, like crash. 182 | --- http_config eval: $::HttpConfig 183 | --- config 184 | location /t { 185 | content_by_lua_block { 186 | math.randomseed(9975098) 187 | 188 | local swrr = require "resty.swrr" 189 | 190 | local servers = { 191 | ["server1"] = 0, 192 | ["server2"] = 0, 193 | ["server3"] = 0, 194 | ["server4"] = 0, 195 | } 196 | 197 | local rr = swrr:new(servers, true) 198 | 199 | for i = 1, 4 do 200 | local id = rr:find() 201 | end 202 | 203 | ngx.say("ok") 204 | } 205 | } 206 | --- request 207 | GET /t 208 | --- response_body 209 | ok 210 | --- no_error_log 211 | [error] 212 | -------------------------------------------------------------------------------- /valgrind.suppress: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Addr1 4 | fun:ngx_init_cycle 5 | fun:ngx_master_process_cycle 6 | fun:main 7 | } 8 | { 9 | 10 | Memcheck:Addr4 11 | fun:ngx_init_cycle 12 | fun:ngx_master_process_cycle 13 | fun:main 14 | } 15 | { 16 | 17 | Memcheck:Cond 18 | fun:ngx_vslprintf 19 | fun:ngx_snprintf 20 | fun:ngx_sock_ntop 21 | fun:ngx_event_accept 22 | fun:ngx_epoll_process_events 23 | fun:ngx_process_events_and_timers 24 | } 25 | { 26 | 27 | Memcheck:Cond 28 | fun:ngx_vslprintf 29 | fun:ngx_snprintf 30 | fun:ngx_sock_ntop 31 | fun:ngx_event_accept 32 | fun:ngx_epoll_process_events 33 | fun:ngx_process_events_and_timers 34 | } 35 | { 36 | 37 | Memcheck:Addr1 38 | fun:ngx_vslprintf 39 | fun:ngx_snprintf 40 | fun:ngx_sock_ntop 41 | fun:ngx_event_accept 42 | } 43 | { 44 | 45 | exp-sgcheck:SorG 46 | fun:ngx_http_lua_ndk_set_var_get 47 | } 48 | { 49 | 50 | exp-sgcheck:SorG 51 | fun:ngx_http_variables_init_vars 52 | fun:ngx_http_block 53 | } 54 | { 55 | 56 | exp-sgcheck:SorG 57 | fun:ngx_conf_parse 58 | } 59 | { 60 | 61 | exp-sgcheck:SorG 62 | fun:ngx_vslprintf 63 | fun:ngx_log_error_core 64 | } 65 | { 66 | 67 | Memcheck:Param 68 | epoll_ctl(event) 69 | fun:epoll_ctl 70 | } 71 | { 72 | 73 | Memcheck:Cond 74 | fun:ngx_conf_flush_files 75 | fun:ngx_single_process_cycle 76 | } 77 | { 78 | 79 | Memcheck:Cond 80 | fun:memcpy 81 | fun:ngx_vslprintf 82 | fun:ngx_log_error_core 83 | fun:ngx_http_charset_header_filter 84 | } 85 | { 86 | 87 | Memcheck:Param 88 | socketcall.setsockopt(optval) 89 | fun:setsockopt 90 | fun:drizzle_state_connect 91 | } 92 | { 93 | 94 | Memcheck:Cond 95 | fun:ngx_conf_flush_files 96 | fun:ngx_single_process_cycle 97 | fun:main 98 | } 99 | { 100 | 101 | Memcheck:Leak 102 | fun:malloc 103 | fun:ngx_alloc 104 | fun:ngx_event_process_init 105 | } 106 | { 107 | 108 | Memcheck:Param 109 | sendmsg(mmsg[0].msg_hdr) 110 | fun:sendmmsg 111 | fun:__libc_res_nsend 112 | } 113 | { 114 | 115 | Memcheck:Param 116 | sendmsg(msg.msg_iov[0]) 117 | fun:__sendmsg_nocancel 118 | fun:ngx_write_channel 119 | fun:ngx_pass_open_channel 120 | fun:ngx_start_cache_manager_processes 121 | } 122 | { 123 | 124 | Memcheck:Cond 125 | fun:ngx_init_cycle 126 | fun:ngx_master_process_cycle 127 | fun:main 128 | } 129 | { 130 | 131 | Memcheck:Cond 132 | fun:index 133 | fun:expand_dynamic_string_token 134 | fun:_dl_map_object 135 | fun:map_doit 136 | fun:_dl_catch_error 137 | fun:do_preload 138 | fun:dl_main 139 | fun:_dl_sysdep_start 140 | fun:_dl_start 141 | } 142 | { 143 | 144 | Memcheck:Param 145 | sendmsg(mmsg[0].msg_hdr) 146 | fun:sendmmsg 147 | fun:__libc_res_nsend 148 | fun:__libc_res_nquery 149 | fun:__libc_res_nquerydomain 150 | fun:__libc_res_nsearch 151 | } 152 | { 153 | 154 | Memcheck:Leak 155 | match-leak-kinds: definite 156 | fun:malloc 157 | fun:ngx_alloc 158 | fun:ngx_set_environment 159 | fun:ngx_single_process_cycle 160 | fun:main 161 | } 162 | { 163 | 164 | Memcheck:Cond 165 | obj:* 166 | } 167 | --------------------------------------------------------------------------------