From a16d0268c023fd3e3470947cc2b00e7e0e46d7a3 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Wed, 29 Jul 2026 21:26:30 +0800 Subject: [PATCH] fix(plugin): preserve plugin-conf state across consumer route merge Several plugins cache request-time state on their conf object: ai-proxy-multi keeps DNS resolution results (_dns_nodes, _nodes_ver, _resolved_endpoint), upstream.lua keeps _nodes_ver, elasticsearch-logger keeps the probed _version. merge_consumer_route deep-copies the whole route_conf, including value.plugins, so every consumer request that misses the merged_route lru cache produces a fresh plugin conf object and loses that cached state -- DNS is re-resolved, the ES version is re-probed, and with multiple consumers the state is fragmented per consumer instead of shared. Without consumer auth the route conf is never copied, so this only regresses on the consumer path. Adds an opts.shallow_prefix option to core.table.deepcopy that shallow-copies members whose parent path starts with the given prefix, and uses { shallow_prefix = 'self.value.plugins' } in merge_consumer_route so the plugin conf objects keep their identity. The plugins container itself is still a fresh table, so overwriting its keys in the merge does not mutate the original; only the plugin confs that carry state stay shared by reference, which restores the non-consumer behavior. Adds t/core/table.t TEST 13 for opts.shallow_prefix. Signed-off-by: AlinsRan --- apisix/core/table.lua | 4 +++- apisix/plugin.lua | 5 ++++- t/core/table.t | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apisix/core/table.lua b/apisix/core/table.lua index 60b0a6eba739..dd767859fd4c 100644 --- a/apisix/core/table.lua +++ b/apisix/core/table.lua @@ -31,6 +31,7 @@ local pairs = pairs local type = type local ngx_re = require("ngx.re") local isarray = require("table.isarray") +local str_has_prefix = require("apisix.core.string").has_prefix local _M = { @@ -130,7 +131,8 @@ do copied[orig] = copy for orig_key, orig_value in pairs(orig) do local path = parent .. "." .. tostring(orig_key) - if opts and array_find(opts.shallows, path) then + if opts and (array_find(opts.shallows, path) or + (opts.shallow_prefix and str_has_prefix(parent, opts.shallow_prefix))) then copy[orig_key] = orig_value else if type(orig_value) == "table" then diff --git a/apisix/plugin.lua b/apisix/plugin.lua index a2898bc0d404..afdf50f0bb2f 100644 --- a/apisix/plugin.lua +++ b/apisix/plugin.lua @@ -818,7 +818,10 @@ local function merge_consumer_route(route_conf, consumer_conf, consumer_group_co return route_conf end - local new_route_conf = core.table.deepcopy(route_conf) + -- the plugins subtree is fully overwritten below, so there is no need to + -- deep-copy it; shallow-copy that subtree to avoid the wasted work + local new_route_conf = core.table.deepcopy(route_conf, + { shallow_prefix = "self.value.plugins" }) if has_group_plugins then for name, conf in pairs(consumer_group_conf.value.plugins) do diff --git a/t/core/table.t b/t/core/table.t index 38616ae535bf..52e55162ee51 100644 --- a/t/core/table.t +++ b/t/core/table.t @@ -359,3 +359,37 @@ tab_copied.a.b.c == tab.a.b.c1: true tab_copied.a.b.c == t1: true tab_copied.x.y == tab.x.y: false tab_copied.x.y == t2: false + + + +=== TEST 13: shallow_prefix +--- config + location /t { + content_by_lua_block { + local core = require("apisix.core") + local deepcopy = core.table.deepcopy + local plugin1 = {name = "plugin1"} + local plugin2 = {name = "plugin2"} + local tab = { + plugins = { + p1 = plugin1, + p2 = plugin2, + }, + } + + local tab_copied = deepcopy(tab, { shallow_prefix = "self.plugins" }) + + tab_copied.plugins.p1 = {name = "plugin1_new_modified"} + tab_copied.plugins.p2.name = "plugin2_modified" + + ngx.say("table copied: ", require("toolkit.json").encode(tab_copied)) + ngx.say("table original: ", require("toolkit.json").encode(tab)) + } + } +--- request +GET /t +--- response_body +table copied: {"plugins":{"p1":{"name":"plugin1_new_modified"},"p2":{"name":"plugin2_modified"}}} +table original: {"plugins":{"p1":{"name":"plugin1"},"p2":{"name":"plugin2_modified"}}} +--- no_error_log +[error]