This is an automated email from the ASF dual-hosted git repository.
guoqqqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new b972ce477b fix(nacos): keep cached nodes when a per-service refresh
fails (#13971)
b972ce477b is described below
commit b972ce477beea5f2140c3182110b9a87c2140c40
Author: lordcheng10 <[email protected]>
AuthorDate: Wed Oct 21 08:54:57 2026 +0800
fix(nacos): keep cached nodes when a per-service refresh fails (#13971)
---
apisix/discovery/nacos/client.lua | 32 +++--
apisix/discovery/nacos/init.lua | 8 +-
t/discovery/nacos2.t | 248 ++++++++++++++++++++++++++++++++++++++
3 files changed, 275 insertions(+), 13 deletions(-)
diff --git a/apisix/discovery/nacos/client.lua
b/apisix/discovery/nacos/client.lua
index 3f674d3a18..3319e64ebe 100644
--- a/apisix/discovery/nacos/client.lua
+++ b/apisix/discovery/nacos/client.lua
@@ -190,9 +190,15 @@ end
--- Fetch instances from a single nacos host for a list of services.
---
---- Returns: nodes_cache (table of key → nodes), service_names (set of key →
true)
+--- Returns: nodes_cache (table of key → nodes),
+--- configured_services (set of key → true)
--- On failure: nil, nil, err_string
---
+--- configured_services holds every service that was queried, including the
+--- services whose query failed. Callers use it to distinguish a service that
+--- is no longer referenced by APISIX configuration from one whose refresh
+--- merely failed, so a failed refresh does not drop a usable cached snapshot.
+---
--- options:
--- default_weight (number) default node weight
--- access_key (string) AK for HMAC-SHA1 signing (optional)
@@ -217,7 +223,7 @@ function _M.fetch_from_host(base_uri, username, password,
services, options)
return nil, nil, err
end
- local service_names = {}
+ local configured_services = {}
local nodes_cache = {}
local had_success = false
@@ -225,6 +231,18 @@ function _M.fetch_from_host(base_uri, username, password,
services, options)
local namespace_id = service_info.namespace_id
local group_name = service_info.group_name
local scheme = service_info.scheme or ''
+
+ local key
+ if key_builder then
+ key = key_builder(namespace_id, group_name,
service_info.service_name)
+ else
+ key = namespace_id .. '.' .. group_name .. '.' ..
service_info.service_name
+ end
+
+ -- Record the key before the query so a failing service is still
+ -- reported as configured by APISIX.
+ configured_services[key] = true
+
local namespace_param = get_namespace_param(namespace_id)
local group_name_param = get_group_name_param(group_name)
local signature_param = _M.get_signed_param(
@@ -238,14 +256,6 @@ function _M.fetch_from_host(base_uri, username, password,
services, options)
else
had_success = true
- local key
- if key_builder then
- key = key_builder(namespace_id, group_name,
service_info.service_name)
- else
- key = namespace_id .. '.' .. group_name .. '.' ..
service_info.service_name
- end
- service_names[key] = true
-
local hosts = data.hosts
if type(hosts) ~= 'table' then
hosts = {}
@@ -275,7 +285,7 @@ function _M.fetch_from_host(base_uri, username, password,
services, options)
return nil, nil, 'all nacos services fetch failed'
end
- return nodes_cache, service_names
+ return nodes_cache, configured_services
end
diff --git a/apisix/discovery/nacos/init.lua b/apisix/discovery/nacos/init.lua
index 1c50870613..13ad660a94 100644
--- a/apisix/discovery/nacos/init.lua
+++ b/apisix/discovery/nacos/init.lua
@@ -98,7 +98,7 @@ local function fetch_full_registry(premature, reg)
if not base_uri then
log.warn("nacos host at index ", idx, " is invalid, skip")
else
- local nodes_cache, service_names, err =
nacos_client.fetch_from_host(
+ local nodes_cache, configured_services, err =
nacos_client.fetch_from_host(
base_uri,
username or reg.username,
password or reg.password,
@@ -120,10 +120,14 @@ local function fetch_full_registry(premature, reg)
dict:set(key, core.json.encode(nodes))
end
+ -- Only drop cached entries for services that are no longer
+ -- referenced by APISIX configuration. A service whose query
+ -- failed is still in configured_services, so its last
+ -- successful snapshot is kept until it is really removed.
local all_keys = dict:get_keys(0)
for _, key in ipairs(all_keys) do
if core.string.has_prefix(key, prefix)
- and not service_names[key] then
+ and not configured_services[key] then
dict:delete(key)
end
end
diff --git a/t/discovery/nacos2.t b/t/discovery/nacos2.t
index 5d57b288ee..36f60f1ed8 100644
--- a/t/discovery/nacos2.t
+++ b/t/discovery/nacos2.t
@@ -386,3 +386,251 @@ GET /hello
qr/server [1-2]/
--- error_log
fetch_from_host: http://127.0.0.1:20998/nacos/v1/ err:all nacos services fetch
failed
+
+
+
+=== TEST 7: keep cached nodes of a service whose refresh fails
+--- yaml_config
+apisix:
+ node_listen: 1984
+deployment:
+ role: data_plane
+ role_data_plane:
+ config_provider: yaml
+discovery:
+ nacos:
+ host:
+ - "http://127.0.0.1:20997"
+ prefix: "/nacos/v1/"
+ fetch_interval: 1
+ weight: 1
+ timeout:
+ connect: 2000
+ send: 2000
+ read: 5000
+--- apisix_yaml
+routes:
+ -
+ uri: /hello
+ upstream:
+ service_name: APISIX-NACOS
+ discovery_type: nacos
+ type: roundrobin
+ -
+ uri: /hello_test_group
+ plugins:
+ proxy-rewrite:
+ uri: /hello
+ upstream:
+ service_name: APISIX-NACOS
+ discovery_type: nacos
+ type: roundrobin
+ discovery_args:
+ group_name: test_group
+#END
+--- http_config
+ server {
+ listen 20997;
+
+ location / {
+ access_by_lua_block {
+ -- let the first query of the test_group service succeed so
that
+ -- its nodes get cached, then fail every later query for it
+ if string.find(ngx.var.args or "", "groupName=test_group") then
+ local hits =
ngx.shared.nacos:incr("test:test_group_queries", 1, 0)
+ if hits > 1 then
+ ngx.exit(502)
+ end
+ end
+ }
+ proxy_pass http://127.0.0.1:8858;
+ }
+ }
+--- config
+ location /t {
+ content_by_lua_block {
+ local http = require("resty.http")
+ local base = "http://127.0.0.1:" .. ngx.var.server_port
+ local httpc = http.new()
+
+ -- wait for the first refresh to cache both services
+ ngx.sleep(2.5)
+
+ local res = httpc:request_uri(base .. "/hello_test_group")
+ if not res or res.status ~= 200 then
+ ngx.say("FAIL: initial request for the test_group service: ",
+ res and res.status or "no response")
+ return
+ end
+
+ -- the test_group query now fails, while the default group one
still
+ -- succeeds, so the refresh commits without the failed service
+ ngx.sleep(3)
+
+ res = httpc:request_uri(base .. "/hello_test_group")
+ if not res or res.status ~= 200 then
+ ngx.say("FAIL: cached nodes dropped after a failed refresh: ",
+ res and res.status or "no response")
+ return
+ end
+
+ res = httpc:request_uri(base .. "/hello")
+ if not res or res.status ~= 200 then
+ ngx.say("FAIL: request for the healthy service: ",
+ res and res.status or "no response")
+ return
+ end
+
+ ngx.say("PASS")
+ }
+ }
+--- timeout: 15
+--- request
+GET /t
+--- response_body
+PASS
+--- error_log
+err:status = 502
+
+
+
+=== TEST 8: delete cached nodes once the service is no longer referenced
+--- yaml_config
+apisix:
+ node_listen: 1984
+--- extra_yaml_config
+discovery:
+ nacos:
+ host:
+ - "http://127.0.0.1:8858"
+ fetch_interval: 1
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local json_decode = require("toolkit.json").decode
+ local http = require("resty.http")
+ local httpc = http.new()
+
+ local code, body = t('/apisix/admin/upstreams/1',
+ ngx.HTTP_PUT,
+ [[{
+ "service_name": "APISIX-NACOS",
+ "discovery_type": "nacos",
+ "type": "roundrobin"
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ code, body = t('/apisix/admin/upstreams/2',
+ ngx.HTTP_PUT,
+ [[{
+ "service_name": "APISIX-NACOS",
+ "discovery_type": "nacos",
+ "type": "roundrobin",
+ "discovery_args": {
+ "group_name": "test_group"
+ }
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ code, body = t('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello",
+ "upstream_id": 1
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ code, body = t('/apisix/admin/routes/2',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/hello_test_group",
+ "upstream_id": 2
+ }]]
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.sleep(2)
+
+ local function dump_services()
+ local res = httpc:request_uri(
+ "http://127.0.0.1:" .. ngx.var.server_port
+ .. "/v1/discovery/nacos/dump", { method = "GET" })
+ if not res or res.status ~= 200 then
+ return nil
+ end
+ return (json_decode(res.body) or {}).services
+ end
+
+ local services = dump_services()
+ if not services
+ or not
services["default/public/DEFAULT_GROUP/APISIX-NACOS"]
+ or not services["default/public/test_group/APISIX-NACOS"]
then
+ ngx.say("FAIL: both services should be cached")
+ return
+ end
+
+ -- remove the test_group service from the APISIX configuration
+ code, body = t('/apisix/admin/routes/2', ngx.HTTP_DELETE)
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ code, body = t('/apisix/admin/upstreams/2', ngx.HTTP_DELETE)
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+
+ ngx.sleep(2)
+
+ services = dump_services()
+ if not services then
+ ngx.say("FAIL: cannot dump nacos services")
+ return
+ end
+
+ if services["default/public/test_group/APISIX-NACOS"] then
+ ngx.say("FAIL: removed service is still cached")
+ return
+ end
+
+ if not services["default/public/DEFAULT_GROUP/APISIX-NACOS"] then
+ ngx.say("FAIL: still referenced service was deleted")
+ return
+ end
+
+ ngx.say("PASS")
+ }
+ }
+--- timeout: 15
+--- request
+GET /t
+--- response_body
+PASS