This is an automated email from the ASF dual-hosted git repository.

AlinsRan 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 2f8178093 fix(healthcheck): purge stale targets and release periodic 
lock (bump lua-resty-healthcheck-api7 to 3.2.2) (#13627)
2f8178093 is described below

commit 2f8178093f2c79040527bc76ccbe42394d4676bb
Author: AlinsRan <[email protected]>
AuthorDate: Wed Jul 1 08:52:12 2026 +0800

    fix(healthcheck): purge stale targets and release periodic lock (bump 
lua-resty-healthcheck-api7 to 3.2.2) (#13627)
---
 apisix-master-0.rockspec           |   2 +-
 t/node/healthcheck-stale-cleanup.t | 116 +++++++++++++++++++++++++++++++++++++
 2 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec
index 2ff4f6c12..69ca2a12d 100644
--- a/apisix-master-0.rockspec
+++ b/apisix-master-0.rockspec
@@ -42,7 +42,7 @@ dependencies = {
     "lua-resty-ngxvar = 0.5.2-0",
     "lua-resty-jit-uuid = 0.0.7-2",
     "lua-resty-ksuid = 1.0.1-0",
-    "lua-resty-healthcheck-api7 = 3.2.1-0",
+    "lua-resty-healthcheck-api7 = 3.2.2-0",
     "api7-lua-resty-jwt = 0.2.6-0",
     "lua-resty-hmac-ffi = 0.06-1",
     "lua-resty-cookie = 0.4.1-1",
diff --git a/t/node/healthcheck-stale-cleanup.t 
b/t/node/healthcheck-stale-cleanup.t
new file mode 100644
index 000000000..09e7782d0
--- /dev/null
+++ b/t/node/healthcheck-stale-cleanup.t
@@ -0,0 +1,116 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+use t::APISIX 'no_plan';
+
+repeat_each(1);
+log_level('warn');
+no_root_location();
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: stale targets are purged for every checker, not just the first one
+# Two upstreams each have active health checks enabled. When their `checks`
+# config changes, the health-check manager rebuilds each checker and marks the
+# old targets via delayed_clear(); the health-check library must then purge the
+# dropped node from the shm target list of *every* checker. A library bug
+# cleaned only the first checker per window, so with multiple health-checked
+# upstreams the others kept their deleted nodes forever -- still reported by 
the
+# control API (apache/apisix#13385) and still actively probed 
(apache/apisix#13141).
+# Reproduces only with multiple upstreams; a single-upstream setup always 
cleans
+# (it is the "first" checker). The reconfigure changes `checks` (not just 
nodes)
+# so it always goes through the delayed_clear rebuild path, independent of any
+# incremental-update optimization.
+--- config
+location /t {
+    content_by_lua_block {
+        local json = require("toolkit.json")
+        local t = require("lib.test_admin").test
+
+        local function put_route(id, uri, nodes, interval)
+            local cfg = {
+                uri = uri,
+                upstream = {
+                    type = "roundrobin",
+                    nodes = nodes,
+                    checks = {
+                        active = {
+                            type = "tcp",
+                            healthy   = { interval = interval, successes = 1 },
+                            unhealthy = { interval = interval, tcp_failures = 
1 },
+                        },
+                    },
+                },
+            }
+            assert(t('/apisix/admin/routes/' .. id, ngx.HTTP_PUT, cfg) < 300)
+        end
+
+        -- two upstreams, each health-checked, each with two nodes
+        put_route(1, "/hello1", { ["127.0.0.1:1980"] = 1, ["127.0.0.1:1981"] = 
1 }, 1)
+        put_route(2, "/hello2", { ["127.0.0.1:1980"] = 1, ["127.0.0.1:1982"] = 
1 }, 1)
+
+        -- traffic instantiates both checkers and registers them with the
+        -- shared active-check timer
+        t('/hello1', ngx.HTTP_GET)
+        t('/hello2', ngx.HTTP_GET)
+        ngx.sleep(2)
+
+        -- count the soon-to-be-removed nodes (1981, 1982) present across all
+        -- checkers. A node in a checker's target list IS being actively 
probed,
+        -- so this is the count of nodes under active health check.
+        local function count_targets(ports)
+            local _, _, res = t('/v1/healthcheck', ngx.HTTP_GET)
+            local n = 0
+            for _, info in ipairs(json.decode(res)) do
+                for _, node in ipairs(info.nodes or {}) do
+                    if ports[node.port] then
+                        n = n + 1
+                    end
+                end
+            end
+            return n
+        end
+
+        local removed = { [1981] = true, [1982] = true }
+        -- both nodes are registered and actively probed before removal
+        ngx.say("probed_before: ", count_targets(removed))
+
+        -- drop one node and change the checks config (interval 1 -> 2) on each
+        -- upstream: the manager rebuilds the checker and delayed_clear()s the
+        -- old targets, so the dropped node must be purged by the library
+        put_route(1, "/hello1", { ["127.0.0.1:1980"] = 1 }, 2)
+        put_route(2, "/hello2", { ["127.0.0.1:1980"] = 1 }, 2)
+        t('/hello1', ngx.HTTP_GET)
+        t('/hello2', ngx.HTTP_GET)
+
+        -- wait past DELAYED_CLEAR_TIMEOUT (10s) plus a cleanup window
+        ngx.sleep(15)
+
+        -- after the purge the removed nodes are gone from every checker, so 
they
+        -- can neither be queried via the control API (#13385) nor probed 
(#13141)
+        ngx.say("stale_after: ", count_targets(removed))
+    }
+}
+--- request
+GET /t
+--- response_body
+probed_before: 2
+stale_after: 0
+--- ignore_error_log
+--- timeout: 30

Reply via email to