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

shreemaan-abhishek 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 d5ea736a7d fix(etcd): check availability before starting watcher 
(#13934)
d5ea736a7d is described below

commit d5ea736a7d1219b7730e20fe067b2449485d6f5b
Author: Shreemaan Abhishek <[email protected]>
AuthorDate: Mon Sep 14 18:37:25 2026 +0800

    fix(etcd): check availability before starting watcher (#13934)
---
 apisix/core/config_etcd.lua        | 61 ++++++++++++++++++++------------------
 t/core/etcd-watch-start-revision.t | 31 +++++++++++++++++++
 2 files changed, 63 insertions(+), 29 deletions(-)

diff --git a/apisix/core/config_etcd.lua b/apisix/core/config_etcd.lua
index 7f41dd7a0a..eebc726511 100644
--- a/apisix/core/config_etcd.lua
+++ b/apisix/core/config_etcd.lua
@@ -128,6 +128,29 @@ local function produce_res(res, err)
 end
 
 
+local function wait_for_etcd_available(etcd_cli, prefix)
+    while true do
+        local res, err = etcd_cli:get(prefix)
+        if not res then
+            log.error("etcd get: ", err)
+            ngx_sleep(3)
+        elseif not (res.body and res.body.header and res.body.header.revision) 
then
+            log.error("etcd response missing header.revision")
+            ngx_sleep(3)
+        else
+            local rev = tonumber(res.body.header.revision)
+            if not rev then
+                log.error("etcd response has invalid header.revision: ",
+                          tostring(res.body.header.revision))
+                ngx_sleep(3)
+            else
+                return rev
+            end
+        end
+    end
+end
+
+
 local function do_run_watch(premature)
     if premature then
         return
@@ -147,35 +170,15 @@ local function do_run_watch(premature)
             error("failed to create etcd instance: " .. string(err))
         end
 
-        -- Watch from the revision the preloaded configuration was read at, so
-        -- that everything written after that snapshot is still delivered. The
-        -- revision cannot be read back from loaded_configuration here:
-        -- core.config.new() removes each entry as it consumes it, so the table
-        -- is empty once every preloaded type has been registered -- and then
-        -- the fallback below would start the watch at the current revision and
-        -- drop every write made since the snapshot.
-        local rev = loaded_configuration_rev or 0
-
-        if rev == 0 then
-            while true do
-                local res, err = watch_ctx.cli:get(watch_ctx.prefix)
-                if not res then
-                    log.error("etcd get: ", err)
-                    ngx_sleep(3)
-                elseif not (res.body and res.body.header and 
res.body.header.revision) then
-                    log.error("etcd response missing header.revision")
-                    ngx_sleep(3)
-                else
-                    rev = tonumber(res.body.header.revision)
-                    if not rev then
-                        log.error("etcd response has invalid header.revision: 
",
-                                  tostring(res.body.header.revision))
-                        ngx_sleep(3)
-                    else
-                        break
-                    end
-                end
-            end
+        -- Config objects take watch_ctx.started as permission to wait only for
+        -- events from the main watcher. Do not publish that state until etcd
+        -- has answered at least one request. The returned revision is only the
+        -- fallback when no configuration was preloaded: using a newer revision
+        -- in place of the snapshot revision would skip intervening writes.
+        local current_rev = wait_for_etcd_available(watch_ctx.cli, 
watch_ctx.prefix)
+        local rev = loaded_configuration_rev
+        if not rev or rev == 0 then
+            rev = current_rev
         end
 
         watch_ctx.rev = rev + 1
diff --git a/t/core/etcd-watch-start-revision.t 
b/t/core/etcd-watch-start-revision.t
index 28eaeecd00..3f5eb70f14 100644
--- a/t/core/etcd-watch-start-revision.t
+++ b/t/core/etcd-watch-start-revision.t
@@ -45,6 +45,9 @@ deployment:
         prefix: "/apisix-watch-start-revision"
         host:
             - "http://127.0.0.1:2379";
+--- extra_yaml_config
+nginx_config:
+    worker_processes: 1
 --- extra_init_by_lua_start
     -- io.popen reports nothing about how curl fared, so the etcd response is
     -- checked instead: a silently failing write here would look exactly like
@@ -89,6 +92,32 @@ deployment:
     -- after the snapshot was taken, before any worker starts watching
     _G.etcd_put_for_test("/apisix-watch-start-revision/routes/1",
         
'{"uri":"/hello","upstream":{"type":"roundrobin","nodes":{"127.0.0.1:1980":1}}}')
+--- extra_init_worker_by_lua
+    -- http_init_worker has created the configuration objects and scheduled the
+    -- main watcher, but nginx timers cannot run until this phase returns. Wrap
+    -- the cached watcher client now to verify that initialising the watcher
+    -- checks etcd availability even when the snapshot supplied its revision.
+    local config_etcd = require("apisix.core.config_etcd")
+    local get_etcd
+    for i = 1, 256 do
+        local name, value = debug.getupvalue(config_etcd.new, i)
+        if not name then
+            break
+        end
+        if name == "get_etcd" then
+            get_etcd = value
+            break
+        end
+    end
+    assert(get_etcd, "get_etcd upvalue not found")
+
+    local etcd_cli = assert(get_etcd())
+    local original_get = etcd_cli.get
+    _G.watch_availability_checks = 0
+    etcd_cli.get = function(self, ...)
+        _G.watch_availability_checks = _G.watch_availability_checks + 1
+        return original_get(self, ...)
+    end
 --- config
     location /t {
         content_by_lua_block {
@@ -108,9 +137,11 @@ deployment:
             end
 
             ngx.say("uri: ", route.value.uri)
+            ngx.say("availability checks: ", _G.watch_availability_checks)
         }
     }
 --- request
 GET /t
 --- response_body
 uri: /hello
+availability checks: 1

Reply via email to