-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat: ai rate limiting redis support #12751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: ai rate limiting redis support #12751
Conversation
|
@Baoyuantop PTAL |
|
@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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
apisix/apisix/plugins/limit-conn/limit-conn-redis.lua
Lines 60 to 81 in 7e907a5
| 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?
There was a problem hiding this comment.
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:
- A PR to improve the tests for the rate-limiting plugins.
- A PR to add support for the log phase.
- A PR to add Redis support to the ai-rate-limiting.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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) |
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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) |
| local remaining | ||
| if commit then | ||
| remaining = stored_remaining | ||
| else | ||
| remaining = stored_remaining - requested_cost | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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) |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
apisix/apisix/plugins/limit-count/init.lua
Line 282 in 896d3c3
| delay, remaining, reset = lim:incoming(key, cost) |
|
|
||
|
|
||
| function _M.incoming(self, key, cost, dry_run) | ||
| if get_phase() == "log" then |
There was a problem hiding this comment.
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.
apisix/apisix/plugins/limit-count/init.lua
Lines 301 to 322 in 896d3c3
| 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 |
|
Hi @beardnick, please check these comments. |
Description
Which issue(s) this PR fixes:
Fixes #12482
Notice
limit-count-redis.luaandlimit-count-redis-cluster.luafiles to ensure they now support rate-limiting during the log phase.limit-conn-redis.luafile as a guide while implementing rate-limiting in the log phase.require("lib.test_redis").flush_all()function to every Redis rate-limiting test case. Without this addition, the tests could fail unpredictably.limit-req-redis.tandlimit-req-redis-cluster.ttest 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