This is an automated email from the ASF dual-hosted git repository.

nic-6443 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e3ad1f107 test(ai-rate-limiting): wait for token usage to reach redis 
between requests (#13841)
5e3ad1f107 is described below

commit 5e3ad1f10782e4790124d73af16092c70e9de7e2
Author: Nic <[email protected]>
AuthorDate: Wed Aug 19 08:52:48 2026 +0800

    test(ai-rate-limiting): wait for token usage to reach redis between 
requests (#13841)
---
 t/lib/test_redis.lua        | 117 +++++++++++++++++++++++++++++++++++---------
 t/plugin/ai-rate-limiting.t |  54 +++++++++++++++-----
 2 files changed, 137 insertions(+), 34 deletions(-)

diff --git a/t/lib/test_redis.lua b/t/lib/test_redis.lua
index 7f097a4608..6947bb01c9 100644
--- a/t/lib/test_redis.lua
+++ b/t/lib/test_redis.lua
@@ -70,7 +70,26 @@ local function auth_if_needed(red, opts)
     return nil, err
 end
 
-local function flush_single(host, port, opts)
+local function release(red, host, port, opts)
+    local keepalive_pool = opts.keepalive_pool
+    if keepalive_pool == nil then
+        keepalive_pool = 0
+    end
+    if keepalive_pool == 0 then
+        local ok_close, close_err = red:close()
+        if not ok_close then
+            log_warn("failed to close redis connection ", host, ":", port, ": 
", close_err)
+        end
+    else
+        local keepalive_timeout = opts.keepalive_timeout or 10000
+        local ok_keepalive, keepalive_err = 
red:set_keepalive(keepalive_timeout, keepalive_pool)
+        if not ok_keepalive then
+            log_warn("failed to set keepalive for redis ", host, ":", port, ": 
", keepalive_err)
+        end
+    end
+end
+
+local function connect(host, port, opts)
     local red = redis:new()
     local connect_timeout = opts.connect_timeout or 1000
     local send_timeout = opts.send_timeout or connect_timeout
@@ -85,35 +104,34 @@ local function flush_single(host, port, opts)
 
     local ok_auth, auth_err = auth_if_needed(red, opts)
     if not ok_auth then
-        local ok_close, close_err = red:close()
-        if not ok_close then
-            log_warn("failed to close redis connection ", host, ":", port, ": 
", close_err)
-        end
+        release(red, host, port, {})
         return nil, auth_err
     end
 
+    if opts.database then
+        local ok_select, select_err = red:select(opts.database)
+        if not ok_select then
+            log_warn("failed to select redis db ", opts.database, ": ", 
select_err)
+            release(red, host, port, {})
+            return nil, select_err
+        end
+    end
+
+    return red
+end
+
+local function flush_single(host, port, opts)
+    local red, err = connect(host, port, opts)
+    if not red then
+        return nil, err
+    end
+
     local _, flush_err = red:flushall()
     if flush_err then
         log_warn("failed to flush redis ", host, ":", port, ": ", flush_err)
     end
 
-    local keepalive_pool = opts.keepalive_pool
-    if keepalive_pool == nil then
-        keepalive_pool = 0
-    end
-    if keepalive_pool == 0 then
-        local ok_close, close_err = red:close()
-        if not ok_close then
-            log_warn("failed to close redis connection ", host, ":", port, ": 
", close_err)
-        end
-    else
-        local keepalive_timeout = opts.keepalive_timeout or 10000
-        keepalive_pool = keepalive_pool or 100
-        local ok_keepalive, keepalive_err = 
red:set_keepalive(keepalive_timeout, keepalive_pool)
-        if not ok_keepalive then
-            log_warn("failed to set keepalive for redis ", host, ":", port, ": 
", keepalive_err)
-        end
-    end
+    release(red, host, port, opts)
 
     return true
 end
@@ -160,6 +178,61 @@ function _M.flush_port(host, port, opts)
     return flush_single(host, port, opts)
 end
 
+-- Rate limit counters are committed from a log phase timer, so a request fired
+-- right after the previous one can still be judged against a budget that looks
+-- unspent. Snapshot the counters with sum_counters() before a request and wait
+-- for the total to grow with wait_counters_above() afterwards, rather than
+-- sleeping a fixed amount: a wait long enough to cover a busy machine would 
roll
+-- over the short time windows some of these tests configure.
+function _M.sum_counters(pattern, opts)
+    opts = opts or {}
+    local host = opts.host or DEFAULT_HOST
+    local port = opts.port or 6379
+
+    local red, err = connect(host, port, opts)
+    if not red then
+        return nil, err
+    end
+
+    local keys, keys_err = red:keys(pattern)
+    if not keys then
+        release(red, host, port, opts)
+        return nil, keys_err
+    end
+
+    local total = 0
+    for _, key in ipairs(keys) do
+        -- a key that expired between keys() and get() reads back as ngx.null,
+        -- which is not an error; keep the two apart so a failed read cannot be
+        -- mistaken for a counter of zero
+        local value, get_err = red:get(key)
+        if not value then
+            release(red, host, port, opts)
+            return nil, get_err
+        end
+        total = total + (tonumber(value) or 0)
+    end
+
+    release(red, host, port, opts)
+
+    return total
+end
+
+function _M.wait_counters_above(pattern, previous, opts)
+    local last_err
+    for _ = 1, 100 do
+        local total, err = _M.sum_counters(pattern, opts)
+        if total and total > previous then
+            return true
+        end
+        last_err = err
+        ngx.sleep(0.01)
+    end
+
+    return nil, "counters matching " .. pattern .. " stayed at " .. previous ..
+                (last_err and (", last error: " .. last_err) or "")
+end
+
 _M.default_ports = DEFAULT_PORTS
 
 return _M
diff --git a/t/plugin/ai-rate-limiting.t b/t/plugin/ai-rate-limiting.t
index 9fcadbc1c7..d4fad74991 100644
--- a/t/plugin/ai-rate-limiting.t
+++ b/t/plugin/ai-rate-limiting.t
@@ -1630,18 +1630,48 @@ passed
 
 
 === TEST 37: redis policy shares counter and rejects the 4th request
---- pipelined_requests eval
-[
-    "POST /ai\n" . "{ \"messages\": [ { \"role\": \"system\", \"content\": 
\"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 
1+1?\"} ] }",
-    "POST /ai\n" . "{ \"messages\": [ { \"role\": \"system\", \"content\": 
\"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 
1+1?\"} ] }",
-    "POST /ai\n" . "{ \"messages\": [ { \"role\": \"system\", \"content\": 
\"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 
1+1?\"} ] }",
-    "POST /ai\n" . "{ \"messages\": [ { \"role\": \"system\", \"content\": 
\"You are a mathematician\" }, { \"role\": \"user\", \"content\": \"What is 
1+1?\"} ] }",
-]
---- more_headers
-Authorization: Bearer token
-X-AI-Fixture: openai/chat-model-echo.json
---- error_code eval
-[200, 200, 200, 503]
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local test_redis = require("lib.test_redis")
+            local COUNTERS = "plugin-ai-rate-limiting:*"
+            local OPTS = {database = 1}
+
+            local httpc = http.new()
+            local codes = {}
+            for i = 1, 4 do
+                local before, before_err = test_redis.sum_counters(COUNTERS, 
OPTS)
+                assert(before, before_err)
+                local res = assert(httpc:request_uri(
+                    "http://127.0.0.1:"; .. ngx.var.server_port .. "/ai",
+                    {
+                        method = "POST",
+                        body = [[{
+                            "messages": [
+                                { "role": "system", "content": "You are a 
mathematician" },
+                                { "role": "user", "content": "What is 1+1?" }
+                            ]
+                        }]],
+                        headers = {
+                            ["Content-Type"] = "application/json",
+                            ["Authorization"] = "Bearer token",
+                            ["X-AI-Fixture"] = "openai/chat-model-echo.json",
+                        }
+                    }
+                ))
+                codes[i] = res.status
+                -- only a request that reached the LLM has usage to commit
+                if res.status == 200 and i < 4 then
+                    assert(test_redis.wait_counters_above(COUNTERS, before, 
OPTS))
+                end
+            end
+            ngx.say(table.concat(codes, ", "))
+        }
+    }
+--- timeout: 10
+--- response_body
+200, 200, 200, 503
 
 
 

Reply via email to