Copilot commented on code in PR #12826:
URL: https://github.com/apache/apisix/pull/12826#discussion_r2901487672


##########
apisix/http/service.lua:
##########
@@ -52,6 +52,19 @@ local function filter(service)
 
     apisix_upstream.filter_upstream(service.value.upstream, service)
 
+    if type(pre_service_or_size) == "number" or not obj then
+        return
+    end
+

Review Comment:
   `filter()` returns immediately for full reloads (`type(pre_service_or_size) 
== "number"`). With incremental routing, service changes can affect computed 
route hosts (routes using `service_id` inherit `service.hosts` during router 
build). If services are reloaded via `need_reload` (compaction/restart), 
`service_version` will change but `need_create_radixtree` will remain false, so 
the router may not be rebuilt and can keep stale service-host-derived matching. 
Consider marking `apisix.router.need_create_radixtree = true` on service full 
reload (and/or providing a reliable end-of-reload signal similar to routes).
   ```suggestion
       if type(pre_service_or_size) == "number" then
           -- full reload of services; router may need rebuild because routes
           -- inheriting service.hosts can be affected
           local ar = require("apisix.router")
           ar.need_create_radixtree = true
           core.log.info("full services reload, rebuild radixtree")
           return
       end
   
       if not obj then
           return
       end
   ```



##########
apisix/router.lua:
##########
@@ -17,33 +17,130 @@
 local require = require
 local http_route = require("apisix.http.route")
 local apisix_upstream = require("apisix.upstream")
-local core    = require("apisix.core")
+local core = require("apisix.core")
 local set_plugins_meta_parent = 
require("apisix.plugin").set_plugins_meta_parent
 local str_lower = string.lower
-local ipairs  = ipairs
+local ipairs = ipairs
+local process = require("ngx.process")
+
 
 local _M = {version = 0.3}
 
 
-local function filter(route)
+_M.need_create_radixtree = true
+
+
+local function sync_tb_create(sync_tb, route)
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "delete" then
+        sync_tb[route.value.id] = {op = "update", cur_route = route,
+                                    last_route = 
sync_tb[route.value.id]["last_route"]}
+    end
+end
+
+
+local function sync_tb_delete(sync_tb, route)
+    local key = route.value.id
+    if not sync_tb[key] then
+        sync_tb[key] = {op = "delete", last_route = route}
+    elseif sync_tb[key]["op"] == "create" then
+        sync_tb[key] = nil
+    elseif sync_tb[key]["op"] == "update" then
+        sync_tb[key] = {op = "delete", last_route = sync_tb[key]["last_route"]}
+    end
+end
+
+
+local function sync_tb_update(sync_tb, pre_route, route)
+
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "update", last_route = pre_route, 
cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "update" then
+        sync_tb[route.value.id] = {
+            op = "update",
+            last_route = sync_tb[route.value.id]["last_route"],
+            cur_route = route
+        }
+    elseif sync_tb[route.value.id]["op"] == "create" then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    end
+end
+
+
+local function filter(route, pre_route_or_size, obj)
     route.orig_modifiedIndex = route.modifiedIndex
 
     route.has_domain = false
-    if not route.value then
+
+    if route.value then
+        set_plugins_meta_parent(route.value.plugins, route)
+        if route.value.host then
+            route.value.host = str_lower(route.value.host)
+        elseif route.value.hosts then
+            for i, v in ipairs(route.value.hosts) do
+                route.value.hosts[i] = str_lower(v)
+            end
+        end
+
+        apisix_upstream.filter_upstream(route.value.upstream, route)
+    end
+
+    if not obj then
+        return
+    end
+    --save sync route and operation type into a map
+    if type(pre_route_or_size) == "number" then
+        if pre_route_or_size == #obj.values then
+            _M.need_create_radixtree = true
+        end
+        return
+    end
+
+    -- to not store changes in privileged agent
+    if process.type() == "privileged agent" then
         return
     end
 
-    set_plugins_meta_parent(route.value.plugins, route)
+    local sync_tb = _M.sync_tb
+    if pre_route_or_size then
+        if route.value then
+            --update route
+            core.log.notice("update routes watched from etcd into radixtree. ",
+                            core.json.delay_encode(route, true))

Review Comment:
   These `core.log.notice(... delay_encode(route, true))` statements will emit 
the full route object (including plugins) at NOTICE level on every watched 
change. For frequent route updates this can significantly increase log 
volume/CPU and may expose sensitive plugin config in logs. Consider lowering to 
`debug` or logging only the route id/op (and maybe key fields) instead of the 
full JSON.



##########
apisix/http/router/radixtree_uri.lua:
##########
@@ -27,14 +29,122 @@ local _M = {version = 0.2}
 
     local uri_routes = {}
     local uri_router
+function _M.incremental_operate_radixtree(router, routes, with_parameter)
+    local op, route, last_route, err
+    local cur_tmp, last_tmp
+    local router_opts = {
+        no_param_match = with_parameter ~= true
+    }
+
+    event.push(event.CONST.BUILD_ROUTER, routes)
+    for k, v in pairs(ar.sync_tb) do
+        op = ar.sync_tb[k]["op"]
+        route = ar.sync_tb[k]["cur_route"]
+        last_route = ar.sync_tb[k]["last_route"]
+        cur_tmp = {}
+        last_tmp = {}
+
+        if route and route.value then
+            local status = core.table.try_read_attr(route, "value", "status")
+            if status and status == 0 then
+                goto CONTINUE
+            end
+
+            local filter_fun, err
+            if route.value.filter_func then
+                filter_fun, err = loadstring(
+                    "return " .. route.value.filter_func,
+                    "router#" .. route.value.id
+                )
+                if not filter_fun then
+                    core.log.error("failed to load filter function: ", err,
+                                    " route id", route.value.id)
+                    return

Review Comment:
   On `filter_func` load failure, the code logs and `return`s from 
`incremental_operate_radixtree`, aborting all remaining pending sync operations 
and leaving `ar.sync_tb` uncleared. The full rebuild path skips only the bad 
route; incremental should similarly skip/mark-rebuild and still clear the 
current sync entry to avoid getting stuck on the same failure forever.
   ```suggestion
                       goto CONTINUE
   ```



##########
apisix/http/router/radixtree_uri.lua:
##########
@@ -27,14 +29,122 @@ local _M = {version = 0.2}
 
     local uri_routes = {}
     local uri_router
+function _M.incremental_operate_radixtree(router, routes, with_parameter)
+    local op, route, last_route, err
+    local cur_tmp, last_tmp
+    local router_opts = {
+        no_param_match = with_parameter ~= true
+    }
+
+    event.push(event.CONST.BUILD_ROUTER, routes)

Review Comment:
   The incremental router update logic in this module is a behavior change 
(create/update/delete without full rebuild) but there are no tests covering it. 
Please add Test::Nginx cases that exercise route create/update/delete via the 
Admin API and verify correct matching (including host matching via 
`service_id`, status enable/disable, and parameterized routes for the 
`with_parameter` variant).



##########
apisix/router.lua:
##########
@@ -17,33 +17,130 @@
 local require = require
 local http_route = require("apisix.http.route")
 local apisix_upstream = require("apisix.upstream")
-local core    = require("apisix.core")
+local core = require("apisix.core")
 local set_plugins_meta_parent = 
require("apisix.plugin").set_plugins_meta_parent
 local str_lower = string.lower
-local ipairs  = ipairs
+local ipairs = ipairs
+local process = require("ngx.process")
+
 
 local _M = {version = 0.3}
 
 
-local function filter(route)
+_M.need_create_radixtree = true
+
+
+local function sync_tb_create(sync_tb, route)
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "delete" then
+        sync_tb[route.value.id] = {op = "update", cur_route = route,
+                                    last_route = 
sync_tb[route.value.id]["last_route"]}
+    end
+end
+
+
+local function sync_tb_delete(sync_tb, route)
+    local key = route.value.id
+    if not sync_tb[key] then
+        sync_tb[key] = {op = "delete", last_route = route}
+    elseif sync_tb[key]["op"] == "create" then
+        sync_tb[key] = nil
+    elseif sync_tb[key]["op"] == "update" then
+        sync_tb[key] = {op = "delete", last_route = sync_tb[key]["last_route"]}
+    end
+end
+
+
+local function sync_tb_update(sync_tb, pre_route, route)
+
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "update", last_route = pre_route, 
cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "update" then
+        sync_tb[route.value.id] = {
+            op = "update",
+            last_route = sync_tb[route.value.id]["last_route"],
+            cur_route = route
+        }
+    elseif sync_tb[route.value.id]["op"] == "create" then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    end
+end
+
+
+local function filter(route, pre_route_or_size, obj)
     route.orig_modifiedIndex = route.modifiedIndex
 
     route.has_domain = false
-    if not route.value then
+
+    if route.value then
+        set_plugins_meta_parent(route.value.plugins, route)
+        if route.value.host then
+            route.value.host = str_lower(route.value.host)
+        elseif route.value.hosts then
+            for i, v in ipairs(route.value.hosts) do
+                route.value.hosts[i] = str_lower(v)
+            end
+        end
+
+        apisix_upstream.filter_upstream(route.value.upstream, route)
+    end
+
+    if not obj then
+        return
+    end
+    --save sync route and operation type into a map
+    if type(pre_route_or_size) == "number" then
+        if pre_route_or_size == #obj.values then
+            _M.need_create_radixtree = true
+        end

Review Comment:
   `load_full_data()` passes a numeric second arg (`#values`) into this filter. 
The current `pre_route_or_size == #obj.values` check can fail when some items 
are invalid and skipped (so `#obj.values < #values`), meaning 
`need_create_radixtree` may never be set on a full reload. That can leave the 
worker using a stale router after `need_reload` reloads config. Consider 
setting `need_create_radixtree` unconditionally on full reload (or using a more 
reliable end-of-reload signal than comparing counts).
   ```suggestion
           -- full data reload: always rebuild radixtree after load_full_data,
           -- since some routes may be invalid and skipped, making #obj.values
           -- smaller than the original size passed in as pre_route_or_size
           _M.need_create_radixtree = true
   ```



##########
apisix/http/router/radixtree_uri.lua:
##########
@@ -27,14 +29,122 @@ local _M = {version = 0.2}
 
     local uri_routes = {}
     local uri_router
+function _M.incremental_operate_radixtree(router, routes, with_parameter)
+    local op, route, last_route, err
+    local cur_tmp, last_tmp
+    local router_opts = {
+        no_param_match = with_parameter ~= true
+    }
+
+    event.push(event.CONST.BUILD_ROUTER, routes)
+    for k, v in pairs(ar.sync_tb) do
+        op = ar.sync_tb[k]["op"]
+        route = ar.sync_tb[k]["cur_route"]
+        last_route = ar.sync_tb[k]["last_route"]
+        cur_tmp = {}
+        last_tmp = {}
+
+        if route and route.value then
+            local status = core.table.try_read_attr(route, "value", "status")
+            if status and status == 0 then

Review Comment:
   In `incremental_operate_radixtree`, when a route is disabled (`status == 0`) 
the code does `goto CONTINUE`, but the `::CONTINUE::` label is *after* 
`ar.sync_tb[k] = nil`. This leaves the sync entry in `ar.sync_tb` forever, so 
every subsequent request will keep re-processing (and skipping) the same entry. 
Clear the entry before jumping (or move the cleanup before the label).
   ```suggestion
               if status and status == 0 then
                   -- clear sync entry for disabled route before skipping 
further processing
                   ar.sync_tb[k] = nil
   ```



##########
apisix/http/router/radixtree_uri.lua:
##########
@@ -27,14 +29,122 @@ local _M = {version = 0.2}
 
     local uri_routes = {}
     local uri_router
+function _M.incremental_operate_radixtree(router, routes, with_parameter)
+    local op, route, last_route, err
+    local cur_tmp, last_tmp
+    local router_opts = {
+        no_param_match = with_parameter ~= true
+    }
+
+    event.push(event.CONST.BUILD_ROUTER, routes)
+    for k, v in pairs(ar.sync_tb) do
+        op = ar.sync_tb[k]["op"]
+        route = ar.sync_tb[k]["cur_route"]
+        last_route = ar.sync_tb[k]["last_route"]
+        cur_tmp = {}
+        last_tmp = {}
+
+        if route and route.value then
+            local status = core.table.try_read_attr(route, "value", "status")
+            if status and status == 0 then
+                goto CONTINUE
+            end
+
+            local filter_fun, err
+            if route.value.filter_func then
+                filter_fun, err = loadstring(
+                    "return " .. route.value.filter_func,
+                    "router#" .. route.value.id
+                )
+                if not filter_fun then
+                    core.log.error("failed to load filter function: ", err,
+                                    " route id", route.value.id)
+                    return
+                end
+
+                filter_fun = filter_fun()
+            end
+
+            cur_tmp = {
+                id = route.value.id,
+                paths = route.value.uris or route.value.uri,
+                methods = route.value.methods,
+                priority = route.value.priority,
+                hosts = route.value.hosts or route.value.host,
+                remote_addrs = route.value.remote_addrs or 
route.value.remote_addr,

Review Comment:
   Incremental route building sets `hosts = route.value.hosts or 
route.value.host`, but the full router build (`create_radixtree_uri_router`) 
also derives hosts from the referenced `service_id` when the route itself has 
no host/hosts. Without matching that logic here, incremental 
create/update/delete can change host-matching semantics (and may fail to find 
the existing route to update/delete). Compute `hosts` the same way as the full 
build (including `service_fetch(service_id)` fallback).



##########
apisix/router.lua:
##########
@@ -17,33 +17,130 @@
 local require = require
 local http_route = require("apisix.http.route")
 local apisix_upstream = require("apisix.upstream")
-local core    = require("apisix.core")
+local core = require("apisix.core")
 local set_plugins_meta_parent = 
require("apisix.plugin").set_plugins_meta_parent
 local str_lower = string.lower
-local ipairs  = ipairs
+local ipairs = ipairs
+local process = require("ngx.process")
+
 
 local _M = {version = 0.3}
 
 
-local function filter(route)
+_M.need_create_radixtree = true
+
+
+local function sync_tb_create(sync_tb, route)
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "delete" then
+        sync_tb[route.value.id] = {op = "update", cur_route = route,
+                                    last_route = 
sync_tb[route.value.id]["last_route"]}
+    end
+end
+
+
+local function sync_tb_delete(sync_tb, route)
+    local key = route.value.id
+    if not sync_tb[key] then
+        sync_tb[key] = {op = "delete", last_route = route}
+    elseif sync_tb[key]["op"] == "create" then
+        sync_tb[key] = nil
+    elseif sync_tb[key]["op"] == "update" then
+        sync_tb[key] = {op = "delete", last_route = sync_tb[key]["last_route"]}
+    end
+end
+
+
+local function sync_tb_update(sync_tb, pre_route, route)
+
+    if not sync_tb[route.value.id] then
+        sync_tb[route.value.id] = {op = "update", last_route = pre_route, 
cur_route = route}
+    elseif sync_tb[route.value.id]["op"] == "update" then
+        sync_tb[route.value.id] = {
+            op = "update",
+            last_route = sync_tb[route.value.id]["last_route"],
+            cur_route = route
+        }
+    elseif sync_tb[route.value.id]["op"] == "create" then
+        sync_tb[route.value.id] = {op = "create", cur_route = route}
+    end
+end
+
+
+local function filter(route, pre_route_or_size, obj)
     route.orig_modifiedIndex = route.modifiedIndex
 
     route.has_domain = false
-    if not route.value then
+
+    if route.value then
+        set_plugins_meta_parent(route.value.plugins, route)
+        if route.value.host then
+            route.value.host = str_lower(route.value.host)
+        elseif route.value.hosts then
+            for i, v in ipairs(route.value.hosts) do
+                route.value.hosts[i] = str_lower(v)
+            end
+        end
+
+        apisix_upstream.filter_upstream(route.value.upstream, route)
+    end
+
+    if not obj then
+        return
+    end
+    --save sync route and operation type into a map
+    if type(pre_route_or_size) == "number" then
+        if pre_route_or_size == #obj.values then
+            _M.need_create_radixtree = true
+        end
+        return
+    end
+
+    -- to not store changes in privileged agent
+    if process.type() == "privileged agent" then
         return
     end
 
-    set_plugins_meta_parent(route.value.plugins, route)
+    local sync_tb = _M.sync_tb
+    if pre_route_or_size then
+        if route.value then
+            --update route
+            core.log.notice("update routes watched from etcd into radixtree. ",
+                            core.json.delay_encode(route, true))
+            local pre_status = pre_route_or_size.value.status
+            local status = route.value.status
+
+            -- sync according status
+            if pre_status == 0 and status == 1 then
+                sync_tb_create(sync_tb, route)
+            elseif pre_status == 1 and status == 0 then
+                sync_tb_delete(sync_tb,  pre_route_or_size)
+            elseif pre_status == 1 and status == 1 then
+                sync_tb_update(sync_tb, pre_route_or_size, route)

Review Comment:
   The sync logic treats `status` as always `0/1`, but APISIX historically 
treats missing `status` as enabled (many existing objects omit it, and schema 
validation does not apply defaults). Here, a nil `status` won't match any 
branch and may prevent create/update/delete from being recorded (and create 
uses `if route.value.status == 1` only). Normalize missing `status` to `1` (for 
both current and previous route) before computing the operation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to