diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua index a91199edd4ee..2d7d07ef3911 100644 --- a/apisix/core/config_etcd.lua +++ b/apisix/core/config_etcd.lua @@ -569,7 +569,7 @@ local function load_full_data(self, dir_res, headers) item.clean_handlers = {} if self.filter then - self.filter(item) + self.filter(item, 1, self) end end @@ -623,7 +623,7 @@ local function load_full_data(self, dir_res, headers) item.clean_handlers = {} if self.filter then - self.filter(item) + self.filter(item, #values, self) end end @@ -771,9 +771,10 @@ local function sync_data(self) return false end + local pre_val local pre_index = self.values_hash[key] if pre_index then - local pre_val = self.values[pre_index] + pre_val = self.values[pre_index] if pre_val then config_util.fire_all_clean_handlers(pre_val) end @@ -831,7 +832,7 @@ local function sync_data(self) -- /plugins' filter need to known self.values when it is called -- so the filter should be called after self.values set. if self.filter then - self.filter(res) + self.filter(res, pre_val, self) end self.conf_version = self.conf_version + 1 diff --git a/apisix/http/route.lua b/apisix/http/route.lua index dbf11abf5e28..6d879414b761 100644 --- a/apisix/http/route.lua +++ b/apisix/http/route.lua @@ -73,6 +73,7 @@ function _M.create_radixtree_uri_router(routes, uri_routes, with_parameter) core.log.info("insert uri route: ", core.json.delay_encode(route.value, true)) core.table.insert(uri_routes, { + id = route.value.id, paths = route.value.uris or route.value.uri, methods = route.value.methods, priority = route.value.priority, @@ -92,7 +93,10 @@ function _M.create_radixtree_uri_router(routes, uri_routes, with_parameter) end end - event.push(event.CONST.BUILD_ROUTER, routes) + local router_module = require("apisix.router") + if router_module.router_http then + event.push(event.CONST.BUILD_ROUTER, routes) + end core.log.info("route items: ", core.json.delay_encode(uri_routes, true)) if with_parameter then diff --git a/apisix/http/router/radixtree_host_uri.lua b/apisix/http/router/radixtree_host_uri.lua index 680a04fbe815..5331b04aeefa 100644 --- a/apisix/http/router/radixtree_host_uri.lua +++ b/apisix/http/router/radixtree_host_uri.lua @@ -25,6 +25,7 @@ local type = type local tab_insert = table.insert local loadstring = loadstring local pairs = pairs +local ar = require("apisix.router") local cached_router_version local cached_service_version local host_router @@ -34,8 +35,23 @@ local only_uri_router local _M = {version = 0.1} +local function get_host_radixtree_route(host_rev, sub_router) + return { + id = host_rev, + paths = host_rev, + sub_router = sub_router, + filter_fun = function(vars, opts, ...) + return sub_router:dispatch(vars.uri, opts, ...) + end, + handler = function (api_ctx, match_opts) + api_ctx.real_curr_req_matched_host = match_opts.matched._path + end + } +end + + local function push_host_router(route, host_routes, only_uri_routes) - if type(route) ~= "table" then + if route == nil or (type(route) ~= "table" or route.value == nil) then return end @@ -70,6 +86,7 @@ local function push_host_router(route, host_routes, only_uri_routes) end local radixtree_route = { + id = route.value.id, paths = route.value.uris or route.value.uri, methods = route.value.methods, priority = route.value.priority, @@ -119,36 +136,212 @@ local function create_radixtree_router(routes) local host_router_routes = {} for host_rev, routes in pairs(host_routes) do local sub_router = router.new(routes) - - core.table.insert(host_router_routes, { - paths = host_rev, - filter_fun = function(vars, opts, ...) - return sub_router:dispatch(vars.uri, opts, ...) - end, - handler = function (api_ctx, match_opts) - api_ctx.real_curr_req_matched_host = match_opts.matched._path - end - }) + core.table.insert(host_router_routes, get_host_radixtree_route(host_rev, sub_router)) end event.push(event.CONST.BUILD_ROUTER, routes) - if #host_router_routes > 0 then - host_router = router.new(host_router_routes) - end + -- create router: host_router + host_router = router.new(host_router_routes) -- create router: only_uri_router only_uri_router = router.new(only_uri_routes) return true end + +local function add_route(host_rev, route, router_opts) + local err + local sub_router = host_router:get_sub_router(host_rev, host_rev, router_opts) + + if sub_router then + err = sub_router:add_route(route, router_opts) + + if err ~= nil then + core.log.error("add route in radixtree sub_router failed. ", + core.json.delay_encode(route), err) + return + end + else + local new_sub_router = router.new({route}) + local host_radix_tree = get_host_radixtree_route(host_rev, new_sub_router) + err = host_router:add_route(host_radix_tree, router_opts) + if err ~= nil then + core.log.error("add route in host radixtree failed. ", + core.json.delay_encode(route), err) + return + end + end +end + + +local function delete_route(host_rev, route, router_opts) + local err + local sub_router = host_router:get_sub_router(host_rev, host_rev, router_opts) + err = sub_router:delete_route(route, router_opts) + + if err ~= nil then + core.log.error("delete route in radixtree sub_router failed. ", + core.json.delay_encode(route), err) + return + end + + local is_empty = sub_router:isempty() + + if is_empty then + err = host_router:delete_route(get_host_radixtree_route(host_rev, nil), router_opts) + + if err ~= nil then + core.log.error("delete route in host radixtree failed. ", + core.json.delay_encode(route), err) + return + end + end +end + + +local function modify_route(host_rev, last_route, route, router_opts) + local sub_router = host_router:get_sub_router(host_rev, host_rev, router_opts) + local err = sub_router:update_route(last_route, route, router_opts) + if err ~= nil then + core.log.error("update route in radixtree sub_router failed. ", + core.json.delay_encode(route), err) + return + end +end + + +local function update_routes(last_host_routes, host_routes, router_opts) + local pre_t = {} + local t = {} + + for host_rev, _ in pairs(last_host_routes) do + pre_t[host_rev] = 1 + end + + for host_rev, _ in pairs(host_routes) do + t[host_rev] = 1 + end + + local common = {} + for k in pairs(pre_t) do + if t[k] then + core.table.insert(common, k) + end + end + + for _, p in ipairs(common) do + modify_route(p, last_host_routes[p], host_routes[p], router_opts) + pre_t[p] = nil + t[p] = nil + end + + + for p in pairs(pre_t) do + delete_route(p, last_host_routes[p], router_opts) + end + + + for p in pairs(t) do + add_route(p, host_routes[p], router_opts) + end + +end + + +local function flat_routes(host_routes, only_uri_routes) + for host_rev, routes in pairs(host_routes) do + host_routes[host_rev] = routes[1] + end + + return host_routes, only_uri_routes and only_uri_routes[1] +end + + +local function incremental_operate_radixtree(routes) + if ar.need_create_radixtree then + core.log.notice("create object of radixtree host uri after load_full_data or init.") + create_radixtree_router(routes) + ar.need_create_radixtree = false + core.table.clear(ar.sync_tb) + return + end + + local op, cur_route, last_route + local router_opts = { + no_param_match = true + } + + event.push(event.CONST.BUILD_ROUTER, routes) + for k, _ in pairs(ar.sync_tb) do + op = ar.sync_tb[k]["op"] + cur_route = ar.sync_tb[k]["cur_route"] + last_route = ar.sync_tb[k]["last_route"] + local err + local host_routes = {} + local only_uri_routes = {} + local last_host_routes = {} + local last_only_uri_routes = {} + + push_host_router(cur_route, host_routes, only_uri_routes) + push_host_router(last_route, last_host_routes, last_only_uri_routes) + + local host_routes, only_uri_route = flat_routes(host_routes, only_uri_routes) + local last_host_routes, last_only_uri_route = + flat_routes(last_host_routes, last_only_uri_routes) + + if not core.table.isempty(host_routes) or not core.table.isempty(last_host_routes) then + if op == "update" then + core.log.notice("update routes watched from etcd into radixtree.") + update_routes(last_host_routes, host_routes, router_opts) + elseif op == "create" then + core.log.notice("create routes watched from etcd into radixtree.") + for host_rev, route in pairs(host_routes) do + add_route(host_rev, route, router_opts) + end + elseif op == "delete" then + core.log.notice("delete routes watched from etcd into radixtree.") + for host_rev, route in pairs(last_host_routes) do + delete_route(host_rev, route, router_opts) + end + end + else + if op == "update" then + err = only_uri_router:update_route(last_only_uri_route, only_uri_route, router_opts) + + if err ~= nil then + core.log.error("update a in into radixtree failed. ", + core.json.delay_encode(only_uri_route), err) + return + end + elseif op == "create" then + err = only_uri_router:add_route(only_uri_route, router_opts) + if err ~= nil then + core.log.error("add route in radixtree failed. ", + core.json.delay_encode(only_uri_route), err) + return + end + elseif op == "delete" then + err = only_uri_router:delete_route(last_only_uri_route, router_opts) + if err ~= nil then + core.log.error("delete route in radixtree failed. ", + core.json.delay_encode(last_only_uri_route), err) + return + end + end + end + ar.sync_tb[k] = nil + end +end + + function _M.match(api_ctx) local user_routes = _M.user_routes local _, service_version = get_services() if not cached_router_version or cached_router_version ~= user_routes.conf_version or not cached_service_version or cached_service_version ~= service_version then - create_radixtree_router(user_routes.values) + incremental_operate_radixtree(user_routes.values) cached_router_version = user_routes.conf_version cached_service_version = service_version end diff --git a/apisix/http/router/radixtree_uri.lua b/apisix/http/router/radixtree_uri.lua index 7c1b5c0c147a..f1178c8942da 100644 --- a/apisix/http/router/radixtree_uri.lua +++ b/apisix/http/router/radixtree_uri.lua @@ -18,6 +18,8 @@ local require = require local core = require("apisix.core") local base_router = require("apisix.http.route") local get_services = require("apisix.http.service").services +local ar = require("apisix.router") +local event = require("apisix.core.event") local cached_router_version local cached_service_version @@ -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, + vars = route.value.vars, + filter_fun = filter_fun, + handler = function(api_ctx, match_opts) + api_ctx.matched_params = nil + api_ctx.matched_route = route + api_ctx.curr_req_matched = match_opts.matched + end + } + end + + if last_route and last_route.value then + last_tmp = { + id = last_route.value.id, + paths = last_route.value.uris or last_route.value.uri, + methods = last_route.value.methods, + priority = last_route.value.priority, + hosts = last_route.value.hosts or last_route.value.host, + remote_addrs = last_route.value.remote_addrs or last_route.value.remote_addr, + vars = last_route.value.vars + } + end + + if op == "update" then + core.log.notice("update routes watched from etcd into radixtree. ", + core.json.delay_encode(route)) + err = router:update_route(last_tmp, cur_tmp, router_opts) + if err ~= nil then + core.log.error("update a route into radixtree failed. ", + core.json.delay_encode(route), err) + return + end + elseif op == "create" then + core.log.notice("create routes watched from etcd into radixtree. ", + core.json.delay_encode(route)) + err = router:add_route(cur_tmp, router_opts) + if err ~= nil then + core.log.error("add routes into radixtree failed. ", + core.json.delay_encode(route), err) + return + end + elseif op == "delete" then + core.log.notice("delete routes watched from etcd into radixtree. ", + core.json.delay_encode(last_route)) + err = router:delete_route(last_tmp, router_opts) + if err ~= nil then + core.log.error("delete a route into radixtree failed. ", + core.json.delay_encode(last_route), err) + return + end + end + + ar.sync_tb[k] = nil + ::CONTINUE:: + end +end + + function _M.match(api_ctx) local user_routes = _M.user_routes local _, service_version = get_services() if not cached_router_version or cached_router_version ~= user_routes.conf_version or not cached_service_version or cached_service_version ~= service_version then - uri_router = base_router.create_radixtree_uri_router(user_routes.values, - uri_routes, false) + if ar.need_create_radixtree then + uri_router = base_router.create_radixtree_uri_router(user_routes.values, + uri_routes, false) + ar.need_create_radixtree = false + core.table.clear(ar.sync_tb) + else + _M.incremental_operate_radixtree(uri_router, + user_routes.values, false) + end + cached_router_version = user_routes.conf_version cached_service_version = service_version end diff --git a/apisix/http/router/radixtree_uri_with_parameter.lua b/apisix/http/router/radixtree_uri_with_parameter.lua index 3f10f4fcac49..00651c774896 100644 --- a/apisix/http/router/radixtree_uri_with_parameter.lua +++ b/apisix/http/router/radixtree_uri_with_parameter.lua @@ -18,6 +18,8 @@ local require = require local core = require("apisix.core") local base_router = require("apisix.http.route") local get_services = require("apisix.http.service").services +local ar = require("apisix.router") +local radixtree_uri = require("apisix.http.router.radixtree_uri") local cached_router_version local cached_service_version @@ -33,8 +35,14 @@ function _M.match(api_ctx) if not cached_router_version or cached_router_version ~= user_routes.conf_version or not cached_service_version or cached_service_version ~= service_version then - uri_router = base_router.create_radixtree_uri_router(user_routes.values, - uri_routes, true) + if ar.need_create_radixtree then + uri_router = base_router.create_radixtree_uri_router(user_routes.values, + uri_routes, true) + ar.need_create_radixtree = false + core.table.clear(ar.sync_tb) + else + radixtree_uri.incremental_operate_radixtree(uri_router, user_routes.values, true) + end cached_router_version = user_routes.conf_version cached_service_version = service_version end diff --git a/apisix/http/service.lua b/apisix/http/service.lua index 66bb210231a6..99661381da98 100644 --- a/apisix/http/service.lua +++ b/apisix/http/service.lua @@ -41,7 +41,7 @@ function _M.services() end -local function filter(service) +local function filter(service, pre_service_or_size, obj) service.has_domain = false if not service.value then return @@ -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 + + -- rebuild radixtree if hosts value changed + if pre_service_or_size then + if not core.table.deep_eq(service.value.hosts, pre_service_or_size.value.hosts) then + local ar = require("apisix.router") + ar.need_create_radixtree = true + core.log.info("service hosts changed, rebuild radixtree") + end + end + core.log.info("filter service: ", core.json.delay_encode(service, true)) end diff --git a/apisix/router.lua b/apisix/router.lua index 82270d1ddd9a..58477df6f81a 100644 --- a/apisix/router.lua +++ b/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) + end - 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) + else + --delete route + core.log.notice("delete routes watched from etcd into radixtree. ", + core.json.delay_encode(route, true)) + if pre_route_or_size.value.status == 1 then + sync_tb_delete(sync_tb, pre_route_or_size) + end end + elseif route.value then + --create route + core.log.notice("create routes watched from etcd into radixtree. ", + core.json.delay_encode(route, true)) + if route.value.status == 1 then + sync_tb_create(sync_tb, route) + end + else + core.log.warn("invalid operation type for a route. ", route.key) + return end - apisix_upstream.filter_upstream(route.value.upstream, route) + core.log.info("filter route: ", core.json.delay_encode(route, true)) end @@ -69,6 +166,7 @@ end function _M.http_init_worker() + _M.sync_tb = {} local conf = core.config.local_conf() local router_http_name = "radixtree_uri" local router_ssl_name = "radixtree_sni"