This is an automated email from the ASF dual-hosted git repository. nic-6443 pushed a commit to branch feat/limit-count-evalsha in repository https://gitbox.apache.org/repos/asf/apisix.git
commit e591108fe24ec77c66b03ab460f1081b2cee155a Author: Nic <[email protected]> AuthorDate: Tue May 12 17:16:00 2026 +0800 perf(limit-count): use evalsha with NOSCRIPT fallback for Redis script execution Replace plain red:eval() with red:evalsha() + NOSCRIPT fallback in limit-count Redis and Redis Cluster backends. This reduces per-request network payload from ~200-500 bytes (full script text) to ~40 bytes (SHA1 digest + arguments) and eliminates Redis-side script parsing after the first call. The script SHA1 is computed client-side at module load time using ngx.sha1_bin(), which works correctly for both standalone Redis and Redis Cluster topologies. On NOSCRIPT (after Redis restart or when hitting a new cluster shard), the fallback to eval() implicitly caches the script for subsequent evalsha calls. --- .../limit-count/limit-count-redis-cluster.lua | 8 +- apisix/plugins/limit-count/limit-count-redis.lua | 8 +- build-apisix-runtime.sh.1 | 236 +++++++++++++++++++++ t/plugin/limit-count-redis.t | 34 +++ 4 files changed, 284 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/limit-count/limit-count-redis-cluster.lua b/apisix/plugins/limit-count/limit-count-redis-cluster.lua index be7029b66..98393db08 100644 --- a/apisix/plugins/limit-count/limit-count-redis-cluster.lua +++ b/apisix/plugins/limit-count/limit-count-redis-cluster.lua @@ -17,6 +17,7 @@ local redis_cluster = require("apisix.utils.rediscluster") local core = require("apisix.core") +local to_hex = require("resty.string").to_hex local setmetatable = setmetatable local tostring = tostring @@ -37,6 +38,7 @@ local script = core.string.compress_script([=[ end return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl} ]=]) +local script_sha = to_hex(ngx.sha1_bin(script)) function _M.new(plugin_name, limit, window, conf) @@ -64,7 +66,11 @@ function _M.incoming(self, key, cost) key = self.plugin_name .. tostring(key) local ttl = 0 - local res, err = red:eval(script, 1, key, limit, window, cost or 1) + local res, err = red:evalsha(script_sha, 1, key, limit, window, cost or 1) + if err and core.string.has_prefix(err, "NOSCRIPT") then + core.log.warn("redis evalsha failed, falling back to eval") + res, err = red:eval(script, 1, key, limit, window, cost or 1) + end if err then return nil, err, ttl diff --git a/apisix/plugins/limit-count/limit-count-redis.lua b/apisix/plugins/limit-count/limit-count-redis.lua index ce78383fe..d9b1a41b8 100644 --- a/apisix/plugins/limit-count/limit-count-redis.lua +++ b/apisix/plugins/limit-count/limit-count-redis.lua @@ -16,6 +16,7 @@ -- local redis = require("apisix.utils.redis") local core = require("apisix.core") +local to_hex = require("resty.string").to_hex local assert = assert local setmetatable = setmetatable local tostring = tostring @@ -38,6 +39,7 @@ local script = core.string.compress_script([=[ end return {redis.call('incrby', KEYS[1], 0 - ARGV[3]), ttl} ]=]) +local script_sha = to_hex(ngx.sha1_bin(script)) function _M.new(plugin_name, limit, window, conf) @@ -65,7 +67,11 @@ function _M.incoming(self, key, cost) key = self.plugin_name .. tostring(key) local ttl = 0 - res, err = red:eval(script, 1, key, limit, window, cost or 1) + res, err = red:evalsha(script_sha, 1, key, limit, window, cost or 1) + if err and core.string.has_prefix(err, "NOSCRIPT") then + core.log.warn("redis evalsha failed, falling back to eval") + res, err = red:eval(script, 1, key, limit, window, cost or 1) + end if err then return nil, err, ttl diff --git a/build-apisix-runtime.sh.1 b/build-apisix-runtime.sh.1 new file mode 100644 index 000000000..92f340878 --- /dev/null +++ b/build-apisix-runtime.sh.1 @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +runtime_version=${runtime_version:-0.0.0} + + +debug_args=${debug_args:-} +ENABLE_FIPS=${ENABLE_FIPS:-"false"} +OPENSSL_CONF_PATH=${OPENSSL_CONF_PATH:-$PWD/conf/openssl3/openssl.cnf} + + +OR_PREFIX=${OR_PREFIX:="/usr/local/openresty"} +OPENSSL_PREFIX=${OPENSSL_PREFIX:=$OR_PREFIX/openssl3} +zlib_prefix=${OR_PREFIX}/zlib +pcre_prefix=${OR_PREFIX}/pcre + +cc_opt=${cc_opt:-"-DNGX_LUA_ABORT_AT_PANIC -I$zlib_prefix/include -I$pcre_prefix/include -I$OPENSSL_PREFIX/include"} +ld_opt=${ld_opt:-"-L$zlib_prefix/lib -L$pcre_prefix/lib -L$OPENSSL_PREFIX/lib -Wl,-rpath,$zlib_prefix/lib:$pcre_prefix/lib:$OPENSSL_PREFIX/lib"} + + +# dependencies for building openresty +OPENSSL_VERSION=${OPENSSL_VERSION:-"3.4.1"} +OPENRESTY_VERSION="1.27.1.2" +ngx_multi_upstream_module_ver="1.3.2" +mod_dubbo_ver="1.0.2" +apisix_nginx_module_ver="1.19.4" +wasm_nginx_module_ver="0.7.0" +lua_var_nginx_module_ver="v0.5.3" +lua_resty_events_ver="0.2.0" + + +install_openssl_3(){ + local fips="" + if [ "$ENABLE_FIPS" == "true" ]; then + fips="enable-fips" + fi + # required for openssl 3.x config + cpanm IPC/Cmd.pm + wget --no-check-certificate https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz + tar xvf openssl-${OPENSSL_VERSION}.tar.gz + cd openssl-${OPENSSL_VERSION}/ + export LDFLAGS="-Wl,-rpath,$zlib_prefix/lib:$OPENSSL_PREFIX/lib" + ./config $fips \ + shared \ + zlib \ + enable-camellia enable-seed enable-rfc3779 \ + enable-cms enable-md2 enable-rc5 \ + enable-weak-ssl-ciphers \ + --prefix=$OPENSSL_PREFIX \ + --libdir=lib \ + --with-zlib-lib=$zlib_prefix/lib \ + --with-zlib-include=$zlib_prefix/include + make -j $(nproc) LD_LIBRARY_PATH= CC="gcc" + sudo make install + if [ -f "$OPENSSL_CONF_PATH" ]; then + sudo cp "$OPENSSL_CONF_PATH" "$OPENSSL_PREFIX"/ssl/openssl.cnf + fi + if [ "$ENABLE_FIPS" == "true" ]; then + $OPENSSL_PREFIX/bin/openssl fipsinstall -out $OPENSSL_PREFIX/ssl/fipsmodule.cnf -module $OPENSSL_PREFIX/lib/ossl-modules/fips.so + sudo sed -i 's@# .include [email protected] '"$OPENSSL_PREFIX"'/ssl/fipsmodule.cnf@g; s/# \(fips = fips_sect\)/\1\nbase = base_sect\n\n[base_sect]\nactivate=1\n/g' $OPENSSL_PREFIX/ssl/openssl.cnf + fi + cd .. +} + + +if ([ $# -gt 0 ] && [ "$1" == "latest" ]) || [ "$runtime_version" == "0.0.0" ]; then + debug_args="--with-debug" +fi + +prev_workdir="$PWD" +repo=$(basename "$prev_workdir") +workdir=$(mktemp -d) +cd "$workdir" || exit 1 + + +install_openssl_3 + +wget --no-check-certificate https://openresty.org/download/openresty-${OPENRESTY_VERSION}.tar.gz +tar -zxvpf openresty-${OPENRESTY_VERSION}.tar.gz > /dev/null + +if [ "$repo" == lua-resty-events ]; then + cp -r "$prev_workdir" ./lua-resty-events-${lua_resty_events_ver} +else + git clone --depth=1 -b $lua_resty_events_ver \ + https://github.com/Kong/lua-resty-events.git \ + lua-resty-events-${lua_resty_events_ver} +fi + +if [ "$repo" == ngx_multi_upstream_module ]; then + cp -r "$prev_workdir" ./ngx_multi_upstream_module-${ngx_multi_upstream_module_ver} +else + git clone --depth=1 -b $ngx_multi_upstream_module_ver \ + https://github.com/api7/ngx_multi_upstream_module.git \ + ngx_multi_upstream_module-${ngx_multi_upstream_module_ver} +fi + +if [ "$repo" == mod_dubbo ]; then + cp -r "$prev_workdir" ./mod_dubbo-${mod_dubbo_ver} +else + git clone --depth=1 -b $mod_dubbo_ver \ + https://github.com/api7/mod_dubbo.git \ + mod_dubbo-${mod_dubbo_ver} +fi + +if [ "$repo" == apisix-nginx-module ]; then + cp -r "$prev_workdir" ./apisix-nginx-module-${apisix_nginx_module_ver} +else + git clone --depth=1 -b $apisix_nginx_module_ver \ + https://github.com/api7/apisix-nginx-module.git \ + apisix-nginx-module-${apisix_nginx_module_ver} +fi + +if [ "$repo" == wasm-nginx-module ]; then + cp -r "$prev_workdir" ./wasm-nginx-module-${wasm_nginx_module_ver} +else + git clone --depth=1 -b $wasm_nginx_module_ver \ + https://github.com/api7/wasm-nginx-module.git \ + wasm-nginx-module-${wasm_nginx_module_ver} +fi + +if [ "$repo" == lua-var-nginx-module ]; then + cp -r "$prev_workdir" ./lua-var-nginx-module-${lua_var_nginx_module_ver} +else + git clone --depth=1 -b $lua_var_nginx_module_ver \ + https://github.com/api7/lua-var-nginx-module \ + lua-var-nginx-module-${lua_var_nginx_module_ver} +fi + +cd ngx_multi_upstream_module-${ngx_multi_upstream_module_ver} || exit 1 +./patch.sh ../openresty-${OPENRESTY_VERSION} +cd .. + +cd apisix-nginx-module-${apisix_nginx_module_ver}/patch || exit 1 +./patch.sh ../../openresty-${OPENRESTY_VERSION} +cd ../.. + +cd wasm-nginx-module-${wasm_nginx_module_ver} || exit 1 +./install-wasmtime.sh +cd .. + + +luajit_xcflags=${luajit_xcflags:="-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT"} +no_pool_patch=${no_pool_patch:-} + +cd openresty-${OPENRESTY_VERSION} || exit 1 + +or_limit_ver=0.09 +if [ ! -d "bundle/lua-resty-limit-traffic-$or_limit_ver" ]; then + echo "ERROR: the official repository of lua-resty-limit-traffic has been updated, please sync to API7's repository." >&2 + exit 1 +else + rm -rf bundle/lua-resty-limit-traffic-$or_limit_ver + limit_ver=1.2.0 + wget "https://github.com/api7/lua-resty-limit-traffic/archive/refs/tags/v$limit_ver.tar.gz" -O "lua-resty-limit-traffic-$limit_ver.tar.gz" + tar -xzf lua-resty-limit-traffic-$limit_ver.tar.gz + mv lua-resty-limit-traffic-$limit_ver bundle/lua-resty-limit-traffic-$or_limit_ver +fi + + +./configure --prefix="$OR_PREFIX" \ + --with-cc-opt="-DAPISIX_RUNTIME_VER=$runtime_version $cc_opt" \ + --with-ld-opt="-Wl,-rpath,$OR_PREFIX/wasmtime-c-api/lib $ld_opt" \ + $debug_args \ + --add-module=../mod_dubbo-${mod_dubbo_ver} \ + --add-module=../ngx_multi_upstream_module-${ngx_multi_upstream_module_ver} \ + --add-module=../apisix-nginx-module-${apisix_nginx_module_ver} \ + --add-module=../apisix-nginx-module-${apisix_nginx_module_ver}/src/stream \ + --add-module=../apisix-nginx-module-${apisix_nginx_module_ver}/src/meta \ + --add-module=../wasm-nginx-module-${wasm_nginx_module_ver} \ + --add-module=../lua-var-nginx-module-${lua_var_nginx_module_ver} \ + --add-module=../lua-resty-events-${lua_resty_events_ver} \ + --with-poll_module \ + --with-pcre-jit \ + --without-http_rds_json_module \ + --without-http_rds_csv_module \ + --without-lua_rds_parser \ + --with-stream \ + --with-stream_ssl_module \ + --with-stream_ssl_preread_module \ + --with-http_v2_module \ + --with-http_v3_module \ + --without-mail_pop3_module \ + --without-mail_imap_module \ + --without-mail_smtp_module \ + --with-http_stub_status_module \ + --with-http_realip_module \ + --with-http_addition_module \ + --with-http_auth_request_module \ + --with-http_secure_link_module \ + --with-http_random_index_module \ + --with-http_gzip_static_module \ + --with-http_sub_module \ + --with-http_dav_module \ + --with-http_flv_module \ + --with-http_mp4_module \ + --with-http_gunzip_module \ + --with-threads \ + --with-compat \ + --with-luajit-xcflags="$luajit_xcflags" \ + $no_pool_patch \ + -j`nproc` + +make -j`nproc` +sudo make install +cd .. + +cd lua-resty-events-${lua_resty_events_ver} || exit 1 +sudo install -d "$OR_PREFIX"/lualib/resty/events/ +sudo install -m 664 lualib/resty/events/*.lua "$OR_PREFIX"/lualib/resty/events/ +sudo install -d "$OR_PREFIX"/lualib/resty/events/compat/ +sudo install -m 644 lualib/resty/events/compat/*.lua "$OR_PREFIX"/lualib/resty/events/compat/ +cd .. + +cd apisix-nginx-module-${apisix_nginx_module_ver} || exit 1 +sudo OPENRESTY_PREFIX="$OR_PREFIX" make install +cd .. + +cd wasm-nginx-module-${wasm_nginx_module_ver} || exit 1 +sudo OPENRESTY_PREFIX="$OR_PREFIX" make install +cd .. + +# package etcdctl +ETCD_ARCH="amd64" +ETCD_VERSION=${ETCD_VERSION:-'3.5.4'} +ARCH=${ARCH:-$(uname -m | tr '[:upper:]' '[:lower:]')} + +if [[ $ARCH == "arm64" ]] || [[ $ARCH == "aarch64" ]]; then + ETCD_ARCH="arm64" +fi + +wget -q https://github.com/etcd-io/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz +tar xf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}.tar.gz +# ship etcdctl under the same bin dir of openresty so we can package it easily +sudo cp etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH}/etcdctl "$OR_PREFIX"/bin/ +rm -rf etcd-v${ETCD_VERSION}-linux-${ETCD_ARCH} diff --git a/t/plugin/limit-count-redis.t b/t/plugin/limit-count-redis.t index 3adeb054a..bc2d261af 100644 --- a/t/plugin/limit-count-redis.t +++ b/t/plugin/limit-count-redis.t @@ -195,6 +195,40 @@ GET /hello +=== TEST 6b: flush redis script cache +--- config + location /t { + content_by_lua_block { + local redis = require("resty.redis") + local red = redis:new() + red:set_timeout(1000) + local ok, err = red:connect("127.0.0.1", 6379) + if not ok then + ngx.say("failed to connect: ", err) + return + end + red:script("FLUSH") + red:flushall() + red:set_keepalive(10000, 100) + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 6c: evalsha NOSCRIPT fallback after SCRIPT FLUSH +--- request +GET /hello +--- error_code: 200 +--- grep_error_log eval +qr/redis evalsha failed, falling back to eval/ +--- grep_error_log_out +redis evalsha failed, falling back to eval + + + === TEST 7: set route, with redis host, port and right password --- config location /t {
