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:
JianFeeeee
2026-10-07 02:28:41 +08:00
parent 7934507179
commit 0858a747ca
4 changed files with 203 additions and 22 deletions

View File

@ -2041,7 +2041,19 @@ func (ps *Plugins) Call(name, fn string, args map[string]interface{}) (interface
if w == nil {
return nil, fmt.Errorf("no state for plugin %s", name)
}
return callOn(w, fn, args)
out, err := callOn(w, fn, args)
// An on-demand call mutates state just as a hook does — quota writes its
// host→adapter memo here, and that is the whole point of the function — but
// markDirty used to fire only from hooks and SetState. quota declares no
// hooks, so every probe it ran through this path lived in memory and the memo
// file was only rewritten when something ELSE dirtied the plugin. The
// staleness is what made the memo look inert from the outside: a negative
// entry appeared only after the next configuration change happened to flush.
//
// Marking here is the same call the hook path already makes, so the cost is the
// same (the saver snapshots on its own schedule, not per call).
ps.markDirtyLocked(p)
return out, err
}
// callOn runs one exported function on a specific VM.