Skip to content

Commit

Permalink
fix: deepcopy should copy same table exactly only once (#11861)
Browse files Browse the repository at this point in the history
Signed-off-by: Nic <[email protected]>
  • Loading branch information
nic-6443 authored Dec 26, 2024
1 parent b62d59d commit 3e5e0eb
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 22 deletions.
29 changes: 20 additions & 9 deletions apisix/core/table.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local newproxy = newproxy
local getmetatable = getmetatable
local setmetatable = setmetatable
local select = select
local tostring = tostring
local new_tab = require("table.new")
local nkeys = require("table.nkeys")
local ipairs = ipairs
Expand Down Expand Up @@ -91,7 +92,7 @@ end
-- @usage
-- local arr = {"a", "b", "c"}
-- local idx = core.table.array_find(arr, "b") -- idx = 2
function _M.array_find(array, val)
local function array_find(array, val)
if type(array) ~= "table" then
return nil
end
Expand All @@ -104,6 +105,7 @@ function _M.array_find(array, val)

return nil
end
_M.array_find = array_find


-- only work under lua51 or luajit
Expand All @@ -117,19 +119,28 @@ end

local deepcopy
do
local function _deepcopy(orig, copied)
-- prevent infinite loop when a field refers its parent
copied[orig] = true
local function _deepcopy(orig, copied, parent, opts)
-- If the array-like table contains nil in the middle,
-- the len might be smaller than the expected.
-- But it doesn't affect the correctness.
local len = #orig
local copy = new_tab(len, nkeys(orig) - len)
-- prevent infinite loop when a field refers its parent
copied[orig] = copy
for orig_key, orig_value in pairs(orig) do
if type(orig_value) == "table" and not copied[orig_value] then
copy[orig_key] = _deepcopy(orig_value, copied)
else
local path = parent .. "." .. tostring(orig_key)
if opts and array_find(opts.shallows, path) then
copy[orig_key] = orig_value
else
if type(orig_value) == "table" then
if copied[orig_value] then
copy[orig_key] = copied[orig_value]
else
copy[orig_key] = _deepcopy(orig_value, copied, path, opts)
end
else
copy[orig_key] = orig_value
end
end
end

Expand All @@ -144,13 +155,13 @@ do

local copied_recorder = {}

function deepcopy(orig)
function deepcopy(orig, opts)
local orig_type = type(orig)
if orig_type ~= 'table' then
return orig
end

local res = _deepcopy(orig, copied_recorder)
local res = _deepcopy(orig, copied_recorder, "self", opts)
_M.clear(copied_recorder)
return res
end
Expand Down
10 changes: 1 addition & 9 deletions apisix/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,7 @@ local function parse_domain_in_route(route)
-- don't modify the modifiedIndex to avoid plugin cache miss because of DNS resolve result
-- has changed

local parent = route.value.upstream.parent
if parent then
route.value.upstream.parent = nil
end
route.dns_value = core.table.deepcopy(route.value)
if parent then
route.value.upstream.parent = parent
route.dns_value.upstream.parent = parent
end
route.dns_value = core.table.deepcopy(route.value, { shallows = { "self.upstream.parent"}})
route.dns_value.upstream.nodes = new_nodes
core.log.info("parse route which contain domain: ",
core.json.delay_encode(route, true))
Expand Down
7 changes: 4 additions & 3 deletions apisix/plugin.lua
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ end


local function merge_service_route(service_conf, route_conf)
local new_conf = core.table.deepcopy(service_conf)
local new_conf = core.table.deepcopy(service_conf, { shallows = {"self.value.upstream.parent"}})
new_conf.value.service_id = new_conf.value.id
new_conf.value.id = route_conf.value.id
new_conf.modifiedIndex = route_conf.modifiedIndex
Expand Down Expand Up @@ -658,7 +658,7 @@ end
local function merge_service_stream_route(service_conf, route_conf)
-- because many fields in Service are not supported by stream route,
-- so we copy the stream route as base object
local new_conf = core.table.deepcopy(route_conf)
local new_conf = core.table.deepcopy(route_conf, { shallows = {"self.value.upstream.parent"}})
if service_conf.value.plugins then
for name, conf in pairs(service_conf.value.plugins) do
if not new_conf.value.plugins then
Expand Down Expand Up @@ -706,7 +706,8 @@ 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)
local new_route_conf = core.table.deepcopy(route_conf,
{ shallows = {"self.value.upstream.parent"}})

if consumer_group_conf then
for name, conf in pairs(consumer_group_conf.value.plugins) do
Expand Down
4 changes: 3 additions & 1 deletion apisix/plugins/ai.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ local default_keepalive_pool = {}

local function create_router_matching_cache(api_ctx)
orig_router_http_matching(api_ctx)
return core.table.deepcopy(api_ctx)
return core.table.deepcopy(api_ctx, {
shallows = { "self.matched_route.value.upstream.parent" }
})
end


Expand Down
144 changes: 144 additions & 0 deletions t/core/table.t
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,147 @@ GET /t
GET /t
--- response_body
ok



=== TEST 8: deepcopy copy same table only once
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local tmp = { name = "tmp", priority = 1, enabled = true }
local origin = { a = { b = tmp }, c = tmp}
local copy = core.table.deepcopy(origin)
if not core.table.deep_eq(copy, origin) then
ngx.say("copy: ", json.encode(expect), ", origin: ", json.encode(actual))
return
end
if copy.a.b ~= copy.c then
ngx.say("copy.a.b should be the same as copy.c")
return
end
ngx.say("ok")
}
}
--- request
GET /t
--- response_body
ok



=== TEST 9: reference same table
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local deepcopy = core.table.deepcopy
local tab1 = {name = "tab1"}
local tab2 = {
a = tab1,
b = tab1
}
local tab_copied = deepcopy(tab2)

ngx.say("table copied: ", require("toolkit.json").encode(tab_copied))

ngx.say("tab1 == tab2.a: ", tab1 == tab2.a)
ngx.say("tab2.a == tab2.b: ", tab2.a == tab2.b)

ngx.say("tab_copied.a == tab1: ", tab_copied.a == tab1)
ngx.say("tab_copied.a == tab_copied.b: ", tab_copied.a == tab_copied.b)
}
}
--- request
GET /t
--- response_body
table copied: {"a":{"name":"tab1"},"b":{"name":"tab1"}}
tab1 == tab2.a: true
tab2.a == tab2.b: true
tab_copied.a == tab1: false
tab_copied.a == tab_copied.b: true



=== TEST 10: reference table self(root node)
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local deepcopy = core.table.deepcopy
local tab1 = {name = "tab1"}
local tab2 = {
a = tab1,
}
tab2.c = tab2

local tab_copied = deepcopy(tab2)

ngx.say("tab_copied.a == tab1: ", tab_copied.a == tab_copied.b)
ngx.say("tab_copied == tab_copied.c: ", tab_copied == tab_copied.c)
}
}
--- request
GET /t
--- response_body
tab_copied.a == tab1: false
tab_copied == tab_copied.c: true



=== TEST 11: reference table self(sub node)
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local deepcopy = core.table.deepcopy
local tab_org = {
a = {
a2 = "a2"
},
}
tab_org.b = tab_org.a

local tab_copied = deepcopy(tab_org)
ngx.say("table copied: ", require("toolkit.json").encode(tab_copied))
ngx.say("tab_copied.a == tab_copied.b: ", tab_copied.a == tab_copied.b)
}
}
--- request
GET /t
--- response_body
table copied: {"a":{"a2":"a2"},"b":{"a2":"a2"}}
tab_copied.a == tab_copied.b: true



=== TEST 12: shallow copy
--- config
location /t {
content_by_lua_block {
local core = require("apisix.core")
local deepcopy = core.table.deepcopy
local t1 = {name = "tab1"}
local t2 = {name = "tab2"}
local tab = {
a = {b = {c = t1}},
x = {y = t2},
}
local tab_copied = deepcopy(tab, { shallows = { "self.a.b.c" }})

ngx.say("table copied: ", require("toolkit.json").encode(tab_copied))

ngx.say("tab_copied.a.b.c == tab.a.b.c1: ", tab_copied.a.b.c == tab.a.b.c)
ngx.say("tab_copied.a.b.c == t1: ", tab_copied.a.b.c == t1)
ngx.say("tab_copied.x.y == tab.x.y: ", tab_copied.x.y == tab.x.y)
ngx.say("tab_copied.x.y == t2: ", tab_copied.x.y == t2)
}
}
--- request
GET /t
--- response_body
table copied: {"a":{"b":{"c":{"name":"tab1"}}},"x":{"y":{"name":"tab2"}}}
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

0 comments on commit 3e5e0eb

Please sign in to comment.