From 06a9058a8b1c96a1a3a41611d699fa88c9030f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=A5=87=E8=87=BB?= Date: Tue, 23 Jun 2026 14:55:12 +0800 Subject: [PATCH] fix(limit-conn): use parent resource key for consumer isolation Previously, when limit-conn was configured at the consumer level, connection limits were applied per-route instead of globally per consumer. This occurred because the key generation concatenated ctx.conf_type and ctx.conf_version, which included route-specific information. This commit fixes the issue by using conf._meta.parent.resource_key for key generation, consistent with the same fix already applied to limit-req (#13019) and limit-count plugins. Now consumer-level connection limits are properly shared across all routes accessed by the same consumer. Fixes #13584 Changes: - Added gen_limit_key() function using parent.resource_key approach - Replaced key = key .. ctx.conf_type .. ctx.conf_version with gen_limit_key(conf, ctx, key) in run_limit_conn() - Updated test cases to match new key format - Added test cases (TEST 35-42) verifying consumer isolation across multiple routes --- apisix/plugins/limit-conn/init.lua | 17 ++- t/plugin/limit-conn.t | 199 ++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 5 deletions(-) diff --git a/apisix/plugins/limit-conn/init.lua b/apisix/plugins/limit-conn/init.lua index 0d45bd29074d..ad20ebcdc83e 100644 --- a/apisix/plugins/limit-conn/init.lua +++ b/apisix/plugins/limit-conn/init.lua @@ -16,6 +16,7 @@ -- local limit_conn_new = require("resty.limit.conn").new local core = require("apisix.core") +local apisix_plugin = require("apisix.plugin") local is_http = ngx.config.subsystem == "http" local sleep = core.sleep local tonumber = tonumber @@ -128,6 +129,17 @@ local function get_rules(ctx, conf) end +local function gen_limit_key(conf, ctx, key) + local parent = conf._meta and conf._meta.parent + if not parent or not parent.resource_key then + core.log.error("failed to generate key invalid parent: ", core.json.encode(parent)) + return nil + end + + return parent.resource_key .. ':' .. apisix_plugin.conf_version(conf) .. ':' .. key +end + + local function create_limit_obj(conf, rule, default_conn_delay) core.log.info("create new limit-conn plugin instance") @@ -190,7 +202,10 @@ local function run_limit_conn(conf, rule, ctx) key = ctx.var["remote_addr"] end - key = key .. ctx.conf_type .. ctx.conf_version + key = gen_limit_key(conf, ctx, key) + if not key then + return 500 + end core.log.info("limit key: ", key) local delay, err = lim:incoming(key, true) diff --git a/t/plugin/limit-conn.t b/t/plugin/limit-conn.t index 93c69730fc62..b0fd6e58b755 100644 --- a/t/plugin/limit-conn.t +++ b/t/plugin/limit-conn.t @@ -623,7 +623,7 @@ GET /test_concurrency 503 503 --- error_log -limit key: 10.10.10.1route +limit key: routes/1: @@ -714,7 +714,7 @@ GET /test_concurrency 503 503 --- error_log -limit key: 10.10.10.2route +limit key: routes/1: @@ -988,7 +988,7 @@ GET /test_concurrency 200 200 --- error_log_like eval -qr/limit key: consumer_jackroute&consumer\d+/ +qr/limit key: routes\/\d+:\d+:consumer_jack/ @@ -1077,7 +1077,7 @@ GET /test_concurrency 503 503 --- error_log_like eval -qr/limit key: consumer_jackroute&consumer\d+/ +qr/limit key: routes\/\d+:\d+:consumer_jack/ @@ -1200,3 +1200,194 @@ GET /t --- error_code: 400 --- response_body {"error_msg":"failed to check the configuration of plugin limit-conn err: property \"allow_degradation\" validation failed: wrong type: expected boolean, got string"} + + +=== TEST 35: create consumer with limit-conn +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/jack', + ngx.HTTP_PUT, + [[{ + "username": "jack", + "plugins": { + "key-auth": { + "key": "jack-key" + }, + "limit-conn": { + "conn": 2, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key": "consumer_name" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + +=== TEST 36: create route 1 with key-auth +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "plugins": { + "key-auth": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + +=== TEST 37: create route 2 with key-auth (different uri, same consumer should share conn limit) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/ip", + "plugins": { + "key-auth": {} + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + +=== TEST 38: consumer jack accesses route 1 twice (within conn limit) +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello" +] +--- more_headers +apikey: jack-key +--- error_code eval +[200, 200] + + +=== TEST 39: consumer jack accesses route 2 - should be rejected (consumer-level conn=2 already used by route1) +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello", + "GET /ip" +] +--- more_headers +apikey: jack-key +--- error_code eval +[200, 200, 503] + + +=== TEST 40: create another consumer bob with separate limit-conn config +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/consumers/bob', + ngx.HTTP_PUT, + [[{ + "username": "bob", + "plugins": { + "key-auth": { + "key": "bob-key" + }, + "limit-conn": { + "conn": 2, + "burst": 0, + "default_conn_delay": 0.1, + "rejected_code": 503, + "key": "consumer_name" + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + +=== TEST 41: bob is isolated from jack - bob should not be affected by jack's connections +--- pipelined_requests eval +[ + "GET /hello", + "GET /hello", + "GET /hello", + "GET /hello" +] +--- more_headers +apikey: bob-key +--- error_code eval +[200, 200, 503, 503] + + +=== TEST 42: clean up test data +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + t('/apisix/admin/routes/1', ngx.HTTP_DELETE) + t('/apisix/admin/routes/2', ngx.HTTP_DELETE) + t('/apisix/admin/consumers/jack', ngx.HTTP_DELETE) + t('/apisix/admin/consumers/bob', ngx.HTTP_DELETE) + ngx.say("done") + } + } +--- request +GET /t +--- response_body +done