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 5300993973 fix(limit-count): keep delayed sync counters per rule 
(#14064)
5300993973 is described below

commit 5300993973a455d5f11ab360bcd3d5af6714a70f
Author: lordcheng10 <[email protected]>
AuthorDate: Fri Oct 30 14:38:03 2026 +0800

    fix(limit-count): keep delayed sync counters per rule (#14064)
---
 apisix/plugins/limit-count/init.lua        |   8 +-
 t/plugin/limit-count-redis-delayed-sync2.t | 134 +++++++++++++++++++++++++++++
 2 files changed, 141 insertions(+), 1 deletion(-)

diff --git a/apisix/plugins/limit-count/init.lua 
b/apisix/plugins/limit-count/init.lua
index d64d824544..7215862526 100644
--- a/apisix/plugins/limit-count/init.lua
+++ b/apisix/plugins/limit-count/init.lua
@@ -425,6 +425,7 @@ local function get_rules(ctx, conf)
         end
         return {
             {
+                idx = 1,
                 count = count,
                 time_window = time_window,
                 key = conf.key,
@@ -451,6 +452,7 @@ local function get_rules(ctx, conf)
             return nil, err2
         end
         core.table.insert(rules, {
+            idx = index,
             count = count,
             time_window = time_window,
             key_type = "constant",
@@ -603,7 +605,11 @@ local function run_rate_limit(conf, rule, ctx, name, cost, 
dry_run)
             lim:log_phase_incoming(key, commit_cost)
             return
         elseif enable_delayed_sync then
-            local extra_key = name .. '#' .. conf.policy
+            -- the delayed syncer flushes every queued key with its own 
limiter,
+            -- so each rule needs a separate syncer (queue and timer). Sharing 
one
+            -- per plugin instance would sync the keys of other rules through 
the
+            -- count/time_window of whichever rule started the timer.
+            local extra_key = name .. '#' .. conf.policy .. '#' .. rule.idx
             if conf._vid then
                 extra_key = extra_key .. '#' .. conf._vid
             end
diff --git a/t/plugin/limit-count-redis-delayed-sync2.t 
b/t/plugin/limit-count-redis-delayed-sync2.t
index 4777f33c91..06f51c640f 100644
--- a/t/plugin/limit-count-redis-delayed-sync2.t
+++ b/t/plugin/limit-count-redis-delayed-sync2.t
@@ -271,3 +271,137 @@ Host: example-7.com
     "X-RateLimit-Remaining: 0",
     "X-RateLimit-Remaining: 0",
 ]
+
+
+
+=== TEST 10: set route with multiple rules and delayed sync
+--- config
+    location /t {
+        content_by_lua_block {
+            local t = require("lib.test_admin")
+            local code, body = t.test('/apisix/admin/routes/echo-multi',
+                ngx.HTTP_PUT,
+                [[{
+                    "uri": "/echo",
+                    "plugins": {
+                        "limit-count": {
+                            "policy": "redis",
+                            "redis_host": "127.0.0.1",
+                            "redis_port": 6379,
+                            "redis_database": 2,
+                            "sync_interval": 0.5,
+                            "show_limit_quota_header": true,
+                            "rejected_code": 503,
+                            "rules": [
+                                {
+                                    "count": 5,
+                                    "time_window": 10,
+                                    "key": "${remote_addr}_per_minute",
+                                    "header_prefix": "rule1"
+                                },
+                                {
+                                    "count": 100,
+                                    "time_window": 20,
+                                    "key": "${remote_addr}_per_hour",
+                                    "header_prefix": "rule2"
+                                }
+                            ]
+                        }
+                    },
+                    "host": "example-10.com",
+                    "upstream": {
+                        "type": "roundrobin",
+                        "nodes": {
+                            "127.0.0.1:1980": 1
+                        }
+                    }
+                }]]
+            )
+            if code >= 300 then
+                ngx.status = code
+            end
+            ngx.say(body)
+        }
+    }
+--- response_body
+passed
+
+
+
+=== TEST 11: each rule keeps its own remaining after delayed sync
+--- config
+    location /t {
+        content_by_lua_block {
+            local http = require("resty.http")
+            local httpc = http.new()
+
+            local function request()
+                local res = httpc:request_uri(
+                    "http://127.0.0.1:"; .. ngx.var.server_port .. "/echo", {
+                    headers = {
+                        Host = "example-10.com",
+                    },
+                })
+                if not res then
+                    return nil, "no response"
+                end
+                return res
+            end
+
+            local function header(res, name)
+                for k, v in pairs(res.headers) do
+                    if k:lower() == name then
+                        return tonumber(v)
+                    end
+                end
+                return nil
+            end
+
+            local res, err = request()
+            if not res then
+                ngx.say("FAIL: request before sync: ", err)
+                return
+            end
+
+            local r1 = header(res, "x-rule1-ratelimit-remaining")
+            local r2 = header(res, "x-rule2-ratelimit-remaining")
+            if not r1 or not r2 then
+                ngx.say("FAIL: missing remaining headers before sync")
+                return
+            end
+
+            if r1 ~= 4 or r2 ~= 99 then
+                ngx.say("FAIL: unexpected remaining before sync, rule1=", r1,
+                        ", rule2=", r2)
+                return
+            end
+
+            -- wait for the delayed sync timer to flush the local deltas to 
Redis
+            ngx.sleep(1)
+
+            res, err = request()
+            if not res then
+                ngx.say("FAIL: request after sync: ", err)
+                return
+            end
+
+            r1 = header(res, "x-rule1-ratelimit-remaining")
+            r2 = header(res, "x-rule2-ratelimit-remaining")
+            if not r1 or not r2 then
+                ngx.say("FAIL: missing remaining headers after sync")
+                return
+            end
+
+            -- the second rule must not be synced through the first rule's
+            -- count/time_window
+            if r1 ~= 3 or r2 ~= 98 then
+                ngx.say("FAIL: rule counters corrupted after sync, rule1=", r1,
+                        ", rule2=", r2)
+                return
+            end
+
+            ngx.say("PASS")
+        }
+    }
+--- response_body
+PASS

Reply via email to