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 3978aa3c5 fix(limit-count): commit() must report the window-weighted 
remaining (#13704)
3978aa3c5 is described below

commit 3978aa3c52c7d4d873c2ba5c6d48456cd92f2c28
Author: Nic <[email protected]>
AuthorDate: Mon Jul 20 12:13:59 2026 +0800

    fix(limit-count): commit() must report the window-weighted remaining 
(#13704)
---
 .../limit-count/sliding-window/sliding-window.lua  | 25 +++++++++-
 t/plugin/limit-count-sliding.t                     | 58 ++++++++++++++++++++++
 2 files changed, 81 insertions(+), 2 deletions(-)

diff --git a/apisix/plugins/limit-count/sliding-window/sliding-window.lua 
b/apisix/plugins/limit-count/sliding-window/sliding-window.lua
index f21e5b8a4..7e6592627 100644
--- a/apisix/plugins/limit-count/sliding-window/sliding-window.lua
+++ b/apisix/plugins/limit-count/sliding-window/sliding-window.lua
@@ -169,6 +169,7 @@ end
 function _M.commit(self, key, cost)
     local now = ngx_now()
     local counter_key = get_counter_key(self, key, now)
+    local last_counter_key = get_counter_key(self, key, now - self.window_size)
     local remaining_time = self.window_size - now % self.window_size
 
     local red_cli, err
@@ -179,18 +180,38 @@ function _M.commit(self, key, cost)
         end
     end
 
+    local red = self.red_cli or red_cli
     local expiry = self.window_size * 2
     local new_count
-    new_count, err = self.store:incr(counter_key, cost, expiry, self.red_cli 
or red_cli)
+    new_count, err = self.store:incr(counter_key, cost, expiry, red)
     if err then
         return nil, err, 0
     end
 
+    -- the delta is already recorded at this point, so a failed read of the
+    -- previous window must not fail the commit -- the caller would retry it
+    -- and double-count the delta. Degrade to a remaining without the previous
+    -- window's share instead.
+    local last_count, last_err = self.store:get(last_counter_key, red)
+    if last_err then
+        log.error("failed to get the last window count: ", last_err)
+    end
+    if not last_count then
+        last_count = 0
+    end
+    if last_count > self.limit then
+        last_count = self.limit
+    end
+
     if red_cli then
         red_cli:set_keepalive(10000, 100)
     end
 
-    local remaining = math_floor(self.limit - new_count)
+    -- report the same window-weighted remaining as incoming(); reporting
+    -- limit - new_count alone hands delayed-sync callers a full budget at
+    -- every window start, degrading the sliding window to a fixed one
+    local estimated_last_window_count = last_count / self.window_size * 
remaining_time
+    local remaining = math_floor(self.limit - new_count - 
estimated_last_window_count)
     return 0, remaining, round_off_decimal_places(remaining_time, 2)
 end
 
diff --git a/t/plugin/limit-count-sliding.t b/t/plugin/limit-count-sliding.t
index e6c069a38..d85316943 100644
--- a/t/plugin/limit-count-sliding.t
+++ b/t/plugin/limit-count-sliding.t
@@ -336,3 +336,61 @@ accept 1 count 1
 accept 1 count 2
 accept 0 count 2
 stored: 2
+
+
+
+=== TEST 9: commit() reports the window-weighted remaining, like incoming()
+# regression: commit() used to report limit - current_count, ignoring the
+# previous window's weighted share. Delayed sync caches that value as the
+# global quota, so every new window started from a full budget and the
+# sliding window degraded into a fixed window.
+--- timeout: 10
+--- config
+    location /t {
+        content_by_lua_block {
+            local sliding_window =
+                
require("apisix.plugins.limit-count.sliding-window.sliding-window")
+            local redis_store =
+                
require("apisix.plugins.limit-count.sliding-window.store.redis")
+            local redis_cli = 
require("apisix.plugins.limit-count.util").redis_cli
+            local conf = {
+                redis_host = "127.0.0.1",
+                redis_port = 6379,
+                redis_database = 1,
+            }
+            local limit, window = 400, 3
+            local lim, err = sliding_window.new_with_red_cli_factory(
+                redis_store, limit, window, redis_cli, conf)
+            if not lim then
+                ngx.say("failed to create limiter: ", err)
+                return
+            end
+
+            -- wait for the first 0.4s of a window so the previous window's
+            -- weight stays within a known band during the call below, with
+            -- headroom for the redis round trips before commit() reads time
+            while ngx.now() % window >= 0.4 do
+                ngx.sleep(0.05)
+            end
+            ngx.update_time()
+
+            local now = ngx.now()
+            local key = "ut-commit-weight-" .. now
+            local last_wid = math.floor((now - window) / window)
+            local red = redis_cli(conf)
+            red:set(("%s.%s.counter"):format(key, last_wid), 300, "EX", 60)
+
+            local _, remaining = lim:commit(key, 20)
+            -- remaining_time is in (2.6, 3], so the previous window weighs
+            -- 300 / 3 * remaining_time = 260..300 and the remaining must be
+            -- 400 - 20 - (260..300) = 80..120; assert up to 130 to keep
+            -- headroom for scheduling delay between the wait and the call
+            if remaining >= 80 and remaining <= 130 then
+                ngx.say("ok")
+            else
+                ngx.say("unexpected remaining: ", remaining)
+            end
+        }
+    }
+--- response_body
+ok

Reply via email to