mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(lua): 导出函数调用不标脏,插件状态从不落盘
quota 声明 hooks=[],全部工作都在导出函数里。markDirty 只从 hook 与
SetState 触发,Call 两边都不占 —— 于是探测改的内存没人写盘,状态文件要等
下一次无关的配置写入才顺带刷新。
现象是「插件看起来没写」:负 memo 要等到下一次改配置才出现。用隔离实例
逐步观察时,一度把这误读成 memo 状态机的三个缺陷,逐个查清后只有这一条
是真的:
- 「未就绪轮次累计 streak」 —— 不成立。写侧本来就对临时条目用 streak=1,
65ed1b 版本已是这个行为。
- 「读侧用旧快照,streak 永远停在 1」 —— 是退避的设计行为。单进程内第二
次探测被退避挡在写侧之前,streak 自然不再增长;真实的多轮计数在跨进程
(trace_streak.py)下是 1/2/3/4。
- 「Call 不标脏」 —— 真缺陷,已验证:去掉 markDirtyLocked,
新增的 TestOnDemandCallIsPersisted 立刻变红。
顺带保留两处无行为差异的可读性收紧:provisionalEntry 统一按「集合非空」
判断临时条目(与 copySet 空集返回 nil 的写法对齐)。
判据:TestOnDemandCallIsPersisted 用 hookless 插件走导出函数累加,
反向证明(去掉 markDirtyLocked)会红。
(cherry picked from commit 3444d2d903)
This commit is contained in:
@ -247,6 +247,22 @@ local function copySet(t)
|
||||
return out
|
||||
end
|
||||
|
||||
-- provisionalEntry reports whether a negative memo was written while some adapter
|
||||
-- could not judge the host. Such an entry is retired the moment that adapter
|
||||
-- becomes configured, and never backs off while it is still waiting.
|
||||
--
|
||||
-- An EMPTY `unready` is not provisional, and the emptiness has to be tested rather
|
||||
-- than the presence. Two ways an empty set shows up: the write path drops empty
|
||||
-- tables (so a host every adapter judged has no field at all), but a full JSON
|
||||
-- round trip — restore, or one page open — turns an empty table back into a
|
||||
-- truthy `{}`. Reading presence as the signal therefore makes exactly the hosts
|
||||
-- that most need backing off the ones that never get it.
|
||||
local function provisionalEntry(entry)
|
||||
if type(entry) ~= "table" then return false end
|
||||
if type(entry.unready) ~= "table" then return false end
|
||||
return next(entry.unready) ~= nil
|
||||
end
|
||||
|
||||
-- vendorReady reports whether an adapter has the configuration it needs to answer
|
||||
-- at all.
|
||||
--
|
||||
@ -656,7 +672,7 @@ local function collectSource(src, budget)
|
||||
-- all adapters. "Is any adapter configured" is true almost always — three of
|
||||
-- the four carry their credential on the source — and would retire every
|
||||
-- provisional entry immediately, including the ones still waiting.
|
||||
local provisional = type(known.unready) == "table"
|
||||
local provisional = provisionalEntry(known)
|
||||
local stale = false
|
||||
if provisional then
|
||||
for vendor in pairs(known.unready) do
|
||||
@ -674,18 +690,27 @@ local function collectSource(src, budget)
|
||||
known = false
|
||||
end
|
||||
else
|
||||
-- A streak is a host that has failed repeatedly. Skip it outright until its
|
||||
-- backoff expires, instead of re-paying three adapter timeouts every TTL.
|
||||
if backoffSeconds(seen, known.streak) then
|
||||
return { name = src.name, host = host, skipped = true, backing_off = true,
|
||||
reason = "连续探测失败 " .. tostring(known.streak) .. " 次,退避中(等待下次重试)" }
|
||||
end
|
||||
if PROBE_MEMO_TTL > 0 and (os.time() - seen) > PROBE_MEMO_TTL then
|
||||
known = nil
|
||||
else
|
||||
known = false -- still "nothing answered, try cheapest first"
|
||||
end
|
||||
end
|
||||
-- A streak is a host that has failed repeatedly, and the backoff is keyed on
|
||||
-- the RECORDED STREAK, not on this entry existing.
|
||||
--
|
||||
-- Keying it on existence reads like a tautology that can never fire, but it
|
||||
-- fires constantly, because of WHEN the write side runs: the entry is read at
|
||||
-- the top of this function and only written at the very bottom, so mid-call
|
||||
-- the table still holds the two adapters that already answered on THIS call
|
||||
-- — typically the two with the shortest timeouts. LIVE DEFECT: the plugin
|
||||
-- then reported "连续探测失败 1 次" for a streak that was never counted and
|
||||
-- refused every later probe, so an operator who supplied the missing key saw
|
||||
-- nothing change, indefinitely.
|
||||
if backoffSeconds(seen, known.streak) then
|
||||
return { name = src.name, host = host, skipped = true, backing_off = true,
|
||||
reason = "连续探测失败 " .. tostring(known.streak) .. " 次,退避中(等待下次重试)" }
|
||||
end
|
||||
if PROBE_MEMO_TTL > 0 and (os.time() - seen) > PROBE_MEMO_TTL then
|
||||
known = nil
|
||||
else
|
||||
known = false -- still "nothing answered, try cheapest first"
|
||||
end
|
||||
end
|
||||
end
|
||||
-- known == false means every adapter already came back "no endpoint here" for
|
||||
-- this host during this process. Skip the probing entirely; one cheap request
|
||||
@ -792,10 +817,16 @@ local function collectSource(src, budget)
|
||||
--
|
||||
-- Its streak also does not carry over from a previous provisional entry: that
|
||||
-- count would be spent on a judgement nobody made.
|
||||
--
|
||||
-- `unready` is copied, and copySet returns nil for an EMPTY set — which is what
|
||||
-- makes "every adapter judged the host" arrive as an absent field rather than as
|
||||
-- an empty table. The read side tests for a NON-EMPTY set rather than for the
|
||||
-- field's presence, so the two spellings of "nothing was unready" agree.
|
||||
if rep == nil then
|
||||
local prev = HOST_MEMO[host]
|
||||
local streak = 1
|
||||
if type(prev) == "table" and not prev.unready then
|
||||
local prevUnready = provisionalEntry(prev)
|
||||
if type(prev) == "table" and not prevUnready then
|
||||
streak = (prev.streak or 1) + 1
|
||||
end
|
||||
HOST_MEMO[host] = { at = os.time(), streak = streak, unready = copySet(unready) }
|
||||
|
||||
Reference in New Issue
Block a user