From 1d291d82a1c7679a3f7e49be86d0cb8185015f32 Mon Sep 17 00:00:00 2001 From: apaloleg Date: Sat, 20 Dec 2025 01:23:49 +0300 Subject: [PATCH] feat: add functionality to operate radixtree. This commit adds some methods to operate radixtree. These methods allows Apache APISIX's radixtree_host_uri router to perform more efficient route updates via the Admin API. - sub_router - another radixtree instance attached to route. - get_sub_router () - provides direct access to a sub-router for subsequent operations (add/update/delete routes) without rebuilding the entire tree. - isempty() - checks whether radixtree contains any routes --- lib/resty/radixtree.lua | 106 +++++++++++++++++++++++++++++-------- t/empty.t | 70 +++++++++++++++++++++++++ t/sub_router.t | 112 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 t/empty.t create mode 100644 t/sub_router.t diff --git a/lib/resty/radixtree.lua b/lib/resty/radixtree.lua index 5be3463..7130011 100644 --- a/lib/resty/radixtree.lua +++ b/lib/resty/radixtree.lua @@ -21,6 +21,7 @@ local ipmatcher = require("resty.ipmatcher") local base = require("resty.core.base") local clone_tab = require("table.clone") +local isempty = require("table.isempty") local lrucache = require("resty.lrucache") local expr = require("resty.expr.v1") local bit = require("bit") @@ -312,6 +313,11 @@ local function remove_route(self, opts) return false end + if #route_arr == 1 then + self.hash_path[path] = nil + return true + end + table.remove(route_arr, idx) return true @@ -413,6 +419,30 @@ local function parse_remote_addr(route_remote_addrs) end +local function define_path_op(path, route_opts, global_opts) + local pos = not global_opts.no_param_match and str_find(path, ':', 1, true) + if pos then + path = path:sub(1, pos - 1) + route_opts.path_op = "<=" + route_opts.path = path + route_opts.param = true + + else + pos = str_find(path, '*', 1, true) + if pos then + if pos ~= #path then + route_opts.param = true + end + path = path:sub(1, pos - 1) + route_opts.path_op = "<=" + else + route_opts.path_op = "=" + end + route_opts.path = path + end +end + + local function common_route_data(path, route, route_opts, global_opts) local method = route.methods local bit_methods @@ -469,27 +499,7 @@ local function common_route_data(path, route, route_opts, global_opts) route_opts.path_org = path route_opts.param = false - local pos = not global_opts.no_param_match and str_find(path, ':', 1, true) - if pos then - path = path:sub(1, pos - 1) - route_opts.path_op = "<=" - route_opts.path = path - route_opts.param = true - - else - pos = str_find(path, '*', 1, true) - if pos then - if pos ~= #path then - route_opts.param = true - end - path = path:sub(1, pos - 1) - route_opts.path_op = "<=" - else - route_opts.path_op = "=" - end - route_opts.path = path - end - + define_path_op(path, route_opts, global_opts) log_info("path: ", route_opts.path, " operator: ", route_opts.path_op) route_opts.metadata = route.metadata @@ -497,6 +507,7 @@ local function common_route_data(path, route, route_opts, global_opts) route_opts.method = bit_methods route_opts.filter_fun = route.filter_fun route_opts.priority = route.priority or 0 + route_opts.sub_router = route.sub_router local err local remote_addrs = route.remote_addrs @@ -1015,4 +1026,57 @@ function _M.dispatch(self, path, opts, ...) end +function _M.get_sub_router(self, id, path, global_opts) + local route_opts = {} + + define_path_op(path, route_opts, global_opts) + path = route_opts.path + + if not self.disable_path_cache_opt and route_opts.path_op == '=' then + local route_arr = self.hash_path[path] + if route_arr == nil or type(route_arr) ~= "table" then + log_info("no route arr exists in hash_path.", path) + return nil + end + + for i, route in ipairs(route_arr) do + if route.id == id then + return route_arr[i].sub_router + end + end + + log_info("cannot find route in hash_path.", path, id) + return nil + end + + local data_idx = radix.radix_tree_find(self.tree, path, #path) + if data_idx == nil then + log_info("cannot find the index in radixtree" .. path) + return nil + end + + local idx = tonumber(ffi_cast('intptr_t', data_idx)) + local routes = self.match_data[idx] + + for i, route in ipairs(routes) do + if route.id == id then + return routes[i].sub_router + end + end + + log_info("cannot find route in match_data.", path, id) + + return nil +end + + +function _M.isempty(self) + if not isempty(self.hash_path) then + return false + end + + return isempty(self.match_data) +end + + return _M diff --git a/t/empty.t b/t/empty.t new file mode 100644 index 0000000..0eceb3d --- /dev/null +++ b/t/empty.t @@ -0,0 +1,70 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use t::RX 'no_plan'; + +repeat_each(1); +run_tests(); + +__DATA__ + +=== TEST 1: isempty() on empty router +--- config + location /t { + content_by_lua_block { + local radix = require("resty.radixtree") + local rx = radix.new({}) + ngx.say(rx:isempty()) + } + } +--- request +GET /t +--- response_body +true +--- no_error_log +[error] + + + +=== TEST 2: isempty on router with simple path +--- config + location /t { + content_by_lua_block { + local radix = require("resty.radixtree") + local rx = radix.new({ + { + paths = "/", + metadata = "metadata /", + }, + }) + ngx.say(rx:isempty()) + } + } +--- request +GET /t +--- response_body +false +--- no_error_log +[error] + + + +=== TEST 3: isempty on router with path with '*' +--- config + location /t { + content_by_lua_block { + local radix = require("resty.radixtree") + local rx = radix.new({ + { + paths = "/*", + metadata = "metadata /", + }, + }) + ngx.say(rx:isempty()) + } + } +--- request +GET /t +--- response_body +false +--- no_error_log +[error] \ No newline at end of file diff --git a/t/sub_router.t b/t/sub_router.t new file mode 100644 index 0000000..2875942 --- /dev/null +++ b/t/sub_router.t @@ -0,0 +1,112 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use t::RX 'no_plan'; + +repeat_each(1); +run_tests(); + +__DATA__ + +=== TEST 1: get_sub_router on exact path match with sub_router +--- config + location /t { + content_by_lua_block { + local radix = require("resty.radixtree") + local router_opts = { + no_param_match = true + } + local rx = radix.new({ + { + id = "1", + paths = string.reverse("example-1.com"), + sub_router = { name = "sub_router 1" }, + metadata = "metadata 1", + }, + { + id = "2", + paths = string.reverse("example-1.com"), + sub_router = { name = "sub_router 2" }, + metadata = "metadata 2", + }, + { + id = "3", + paths = string.reverse("example-2.com"), + sub_router = { name = "sub_router 3" }, + metadata = "metadata 3", + } + }) + + + -- step 1. get existing subrouter with id = '1' + ngx.say(rx:get_sub_router("1", string.reverse("example-1.com"), router_opts).name) + + -- step 2. get existing subrouter with id = '2' + ngx.say(rx:get_sub_router("2", string.reverse("example-1.com"), router_opts).name) + + -- step 3. get existing subrouter with id = '3' + ngx.say(rx:get_sub_router("3", string.reverse("example-2.com"), router_opts).name) + + -- step 4. get subrouter with not existing 'id', but existing path + ngx.say(rx:get_sub_router("5", string.reverse("example-1.com"), router_opts)) + + -- step 5. get subrouter with not existing 'path' + ngx.say(rx:get_sub_router("1", string.reverse("example-10.com"), router_opts)) + } + } +--- request +GET /t +--- response_body +sub_router 1 +sub_router 2 +sub_router 3 +nil +nil +--- no_error_log +[error] + + + +=== TEST 2: get_sub_router on '*' path match with sub_router +--- config + location /t { + content_by_lua_block { + local radix = require("resty.radixtree") + local router_opts = { + no_param_match = true + } + local rx = radix.new({ + { + id = "1", + paths = string.reverse("*.example.com"), + sub_router = { name = "sub_router 1" }, + metadata = "metadata 1", + }, + { + id = "2", + paths = string.reverse("test.example.com"), + sub_router = { name = "sub_router 2" }, + metadata = "metadata 2", + }, + }) + + -- step 1. get existing subrouter with id = '1' + ngx.say(rx:get_sub_router("1", string.reverse("*.example.com"), router_opts).name) + -- step 2. get existing subrouter with id = '2' + ngx.say(rx:get_sub_router("2", string.reverse("test.example.com"), router_opts).name) + -- step 3. get subrouter with not existing 'id', but existing path + ngx.say(rx:get_sub_router("3", string.reverse("*.example.com"), router_opts)) + -- step 4. get subrouter with not existing 'path' + ngx.say(rx:get_sub_router("1", string.reverse("*.example-1.com"), router_opts)) + } + } +--- request +GET /t +--- response_body +sub_router 1 +sub_router 2 +nil +nil +--- no_error_log +[error] + +