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 {

Reply via email to