Skip to content

Conversation

@beardnick
Copy link
Contributor

@beardnick beardnick commented Nov 16, 2025

Description

Which issue(s) this PR fixes:

Fixes #12482

Notice
  • I have updated the limit-count-redis.lua and limit-count-redis-cluster.lua files to ensure they now support rate-limiting during the log phase.
  • I referred to the limit-conn-redis.lua file as a guide while implementing rate-limiting in the log phase.
  • To ensure a clean testing environment, I added the require("lib.test_redis").flush_all() function to every Redis rate-limiting test case. Without this addition, the tests could fail unpredictably.
  • I have adjusted the expected results in limit-req-redis.t and limit-req-redis-cluster.t test cases. After thorough verification, the correct behavior is [200, 403, 403, 403]. Previously, the results appeared as [403, 403, 403, 403] due to Redis not being properly cleaned beforehand.

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. enhancement New feature or request labels Nov 16, 2025
@beardnick beardnick changed the title Feature/ai rate limiting redis support feat: ai rate limiting redis support Nov 16, 2025
@beardnick
Copy link
Contributor Author

@Baoyuantop PTAL

@membphis
Copy link
Member

@beardnick many thx for your contribution, some CI tests are failed, you can fix them


local remaining = res[1]
ttl = res[2]
local function log_phase_incoming_thread(premature, self, key, cost)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should be placed in the ai-rate-limiting plugin, not in the limit-count plugin itself, because this code is only useful for ai-rate-limiting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. Similar logic also appears in the limit-conn.

local function leaving_thread(premature, self, key, req_latency)
local conf = self.conf
local red, err = redis.new(conf)
if not red then
return red, err
end
return util.leaving(self, red, key, req_latency)
end
function _M.leaving(self, key, req_latency)
-- log_by_lua can't use cosocket
local ok, err = ngx_timer_at(0, leaving_thread, self, key, req_latency)
if not ok then
core.log.error("failed to create timer: ", err)
return nil, err
end
return ok
end

If I put this logic into ai-rate-limiting plugin, it will make the plugin too complicated. I have to copy a lot of logic from limit-count. Currently, the ai-rate-limiting is just a simple wrapper of limit-count.

Or, do you think I need to rewrite the ai-rate-limiting to something like limit-ai-redis.lua and limit-ai-redis-cluster.lua?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is quite large and hard to review. If you agree, I can split it into 3 separate PRs:

  1. A PR to improve the tests for the rate-limiting plugins.
  2. A PR to add support for the log phase.
  3. A PR to add Redis support to the ai-rate-limiting.

Copy link
Member

@nic-6443 nic-6443 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look like the changes in this PR should be limited to the ai-rate-limiting plugin and minor tweaks to the limit-count plugin. The current PR modifies too much code and test cases for other rate limiting plugins, expanding its scope. We should minimize unnecessary code changes.

=== TEST 21: set route with Redis policy
Copy link
Contributor

@AlinsRan AlinsRan Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recommended to use multi worker testing for Redis related tasks in the new test file ai-rate-limiting-redis.t.

Comment on lines +90 to +95
local commit = true
if dry_run ~= nil then
commit = not dry_run
end

local delay, remaining, ttl = util.redis_incoming(self, red, key, commit, cost)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local commit = true
if dry_run ~= nil then
commit = not dry_run
end
local delay, remaining, ttl = util.redis_incoming(self, red, key, commit, cost)
local delay, remaining, ttl = util.redis_incoming(self, red, key, not commit, cost)

Comment on lines +37 to +39
local requested_cost = cost or 1
local script_cost = commit and requested_cost or 0
local res, err = red:eval(commit_script, 1, key, limit, window, script_cost)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local requested_cost = cost or 1
local script_cost = commit and requested_cost or 0
local res, err = red:eval(commit_script, 1, key, limit, window, script_cost)
local res, err = red:eval(commit_script, 1, key, limit, window, commit and cost or 0)

Comment on lines +51 to +56
local remaining
if commit then
remaining = stored_remaining
else
remaining = stored_remaining - requested_cost
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local remaining
if commit then
remaining = stored_remaining
else
remaining = stored_remaining - requested_cost
end
local remaining = stored_remaining - (commit and 0 or cost)

return 0, remaining, ttl
end

function _M.redis_log_phase_incoming(self, red, key, cost)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's redundant. We can call redis_incoming.

--- more_headers
Authorization: Bearer token
--- error_code eval
[200, 200, 200, 503]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's necessary to check the response header:

--- response_headers eval
[
    "X-AI-RateLimit-Remaining-ai-proxy-openai: 29",
    "X-AI-RateLimit-Remaining-ai-proxy-openai: 19",
    "X-AI-RateLimit-Remaining-ai-proxy-openai: 9",
    "X-AI-RateLimit-Remaining-ai-proxy-openai: 0",
]

--- more_headers
Authorization: Bearer token
--- error_code eval
[200, 200, 200, 503]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

--- config
location /t {
content_by_lua_block {
require("lib.test_redis").flush_all()
Copy link
Contributor

@AlinsRan AlinsRan Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be globally initialized and reset without the need to call it in every test chunk, and the same applies to other test files.

add_block_preprocessor(sub {
    my ($block) = @_;
    my $extra_init_worker_by_lua = <<_EOC_;
        require("lib.test_redis").flush_all()
_EOC_

    $block->set_value("extra_init_worker_by_lua", $extra_init_worker_by_lua);
});

end


function _M.incoming(self, key, cost, dry_run)
Copy link
Contributor

@AlinsRan AlinsRan Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be consistent with local and recommend using the commit parameter instead of dry_run.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that the modification of the init.lua file was overlooked, and the caller did not pass the dry_run parameter.

delay, remaining, reset = lim:incoming(key, cost)



function _M.incoming(self, key, cost, dry_run)
if get_phase() == "log" then
Copy link
Contributor

@AlinsRan AlinsRan Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use a new function to handle the log phase instead of dealing with it within the incoming function. This is because we need to mock parameters like remaining, which isn't reasonable, and the log phase doesn't require a status code.

if not delay then
local err = remaining
if err == "rejected" then
-- show count limit header when rejected
if conf.show_limit_quota_header and set_header then
core.response.set_header(set_limit_headers.limit_header, conf.count,
set_limit_headers.remaining_header, 0,
set_limit_headers.reset_header, reset)
end
if conf.rejected_msg then
return conf.rejected_code, { error_msg = conf.rejected_msg }
end
return conf.rejected_code
end
core.log.error("failed to limit count: ", err)
if conf.allow_degradation then
return
end
return 500, {error_msg = "failed to limit count"}
end

@Baoyuantop
Copy link
Contributor

Hi @beardnick, please check these comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

help request: How to config ai-rate-limiting plugin's counter to redis, as limit-count plugin

5 participants