@@ -236,7 +236,6 @@ local hcs = setmetatable({}, {
236236})
237237
238238local active_check_timer
239- local last_cleanup_check
240239
241240-- serialize a table to a string
242241local serialize = codec .encode
@@ -1726,26 +1725,36 @@ function _M.new(opts)
17261725 self :log (DEBUG , " worker " , ngx_worker_id (), " (pid: " , ngx_worker_pid (), " ) " ,
17271726 " starting active check timer" )
17281727 local shm , key = self .shm , self .PERIODIC_LOCK
1729- last_cleanup_check = ngx_now ()
1728+ local cleanup_key = key .. " :cleanup "
17301729 active_check_timer , err = resty_timer ({
17311730 recurring = true ,
17321731 interval = CHECK_INTERVAL ,
17331732 jitter = CHECK_JITTER ,
17341733 detached = false ,
17351734 expire = function ()
17361735
1737- if get_periodic_lock (shm , key ) then
1738- active_check_timer .interval = CHECK_INTERVAL
1739- renew_periodic_lock (shm , key )
1740- else
1741- active_check_timer .interval = CHECK_INTERVAL * 10
1742- return
1743- end
1744-
17451736 local cur_time = ngx_now ()
1746- for _ , checker_obj in pairs (hcs ) do
17471737
1748- if (last_cleanup_check + CLEANUP_INTERVAL ) < cur_time then
1738+ -- Stale-target cleanup is decoupled from active probing and the
1739+ -- periodic lock. A passive-only deployment (checks.passive but no
1740+ -- active interval) has no active checker on any worker, yet still marks
1741+ -- targets via delayed_clear() and must purge them; gating cleanup on
1742+ -- the periodic lock (which only an active worker ever holds) would leak
1743+ -- those targets forever (apache/apisix#13385).
1744+ --
1745+ -- A single worker per window is elected via an atomic shm:add on
1746+ -- cleanup_key (TTL = CLEANUP_INTERVAL): any worker can win, including a
1747+ -- passive-only one, so the leak fix holds, while the other workers skip
1748+ -- the pass and do not all contend on each checker's TARGET_LIST_LOCK.
1749+ -- The elected worker purges every checker in its own `hcs` in one pass.
1750+ -- Purging writes to the shared shm target_list, so cleaning a checker on
1751+ -- any one worker is globally effective; if checkers are distributed
1752+ -- asymmetrically across workers, a given upstream is purged in whichever
1753+ -- window a worker that owns its checker wins the election -- eventually
1754+ -- consistent rather than every-worker-every-window.
1755+ local won , add_err = shm :add (cleanup_key , ngx_worker_pid (), CLEANUP_INTERVAL )
1756+ if won then
1757+ for _ , checker_obj in pairs (hcs ) do
17491758 -- clear targets marked for delayed removal
17501759 locking_target_list (checker_obj , function (target_list )
17511760 local removed_targets = {}
@@ -1774,10 +1783,45 @@ function _M.new(opts)
17741783 end
17751784 end
17761785 end )
1786+ end
1787+ elseif add_err ~= " exists" then
1788+ ngx_log (ERR , " failed to elect cleanup worker for '" , cleanup_key , " ': " , add_err )
1789+ end
1790+
1791+ -- The periodic lock distributes ACTIVE probing to a single worker. A
1792+ -- worker with no active checker must release the lock (if it holds it)
1793+ -- and back off, so a worker that still owns active checkers can take
1794+ -- over; otherwise active health checks stay stuck after a disable ->
1795+ -- re-enable cycle (apache/apisix#13235). This is gated on
1796+ -- has_active_checker rather than on `hcs` being non-empty, because a
1797+ -- disabled checker lingers in the weak `hcs` table with active=false
1798+ -- until it is garbage collected.
1799+ local has_active_checker = false
1800+ for _ , checker_obj in pairs (hcs ) do
1801+ if checker_obj .checks .active .healthy .active or
1802+ checker_obj .checks .active .unhealthy .active then
1803+ has_active_checker = true
1804+ break
1805+ end
1806+ end
17771807
1778- last_cleanup_check = cur_time
1808+ if not has_active_checker then
1809+ -- release the lock only if we are still the holder, so a worker that
1810+ -- still owns active checkers can take over
1811+ if shm :get (key ) == ngx_worker_pid () then
1812+ shm :delete (key )
17791813 end
1814+ return
1815+ end
17801816
1817+ if get_periodic_lock (shm , key ) then
1818+ renew_periodic_lock (shm , key )
1819+ else
1820+ return
1821+ end
1822+
1823+ -- active probing: only the periodic-lock holder runs the probes
1824+ for _ , checker_obj in pairs (hcs ) do
17811825 if checker_obj .checks .active .healthy .active and
17821826 (checker_obj .checks .active .healthy .last_run +
17831827 checker_obj .checks .active .healthy .interval <= cur_time )
@@ -1868,4 +1912,14 @@ function _M.get_target_list(name, shm_name)
18681912end
18691913
18701914
1915+ if TESTING then
1916+ -- test-only hook: shorten the stale-target cleanup window so the periodic
1917+ -- cleanup path can be exercised deterministically without waiting for the
1918+ -- default CLEANUP_INTERVAL (CHECK_INTERVAL * 25).
1919+ function _M ._set_cleanup_interval (interval )
1920+ CLEANUP_INTERVAL = interval
1921+ end
1922+ end
1923+
1924+
18711925return _M
0 commit comments