mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17: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:
@ -281,3 +281,60 @@ func TestCloseFlushesTail(t *testing.T) {
|
||||
t.Errorf("flushed n = %v, want 1", saved.State["n"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnDemandCallIsPersisted guards the persistence gap that the quota memo fell
|
||||
// into, and it needs a plugin with NO hooks to see it.
|
||||
//
|
||||
// markDirty fired from two places: hooks and SetState. `Call` — the on-demand path
|
||||
// a plugin page uses — fired from neither, so a plugin that does all its work in
|
||||
// exported functions (quota: it declares no hooks at all) mutated state that
|
||||
// nothing ever wrote. The visible symptom was a health probe that looked inert
|
||||
// from outside: the memo changed in memory and the file kept the previous
|
||||
// generation until some unrelated configuration write happened to flush it.
|
||||
//
|
||||
// A hookless plugin is the whole point of the fixture: the counter above
|
||||
// accumulates through a hook, which is the path that already persisted, so it
|
||||
// cannot see this defect.
|
||||
func TestOnDemandCallIsPersisted(t *testing.T) {
|
||||
ps, _ := newPersistVM(t)
|
||||
if err := ps.LoadSource("ondemand", `
|
||||
local plugin = { name = "ondemand", version = "1.0" }
|
||||
plugin.state = { n = 0 }
|
||||
plugin.exports = { "bump" }
|
||||
function plugin.bump(payload)
|
||||
plugin.state.n = plugin.state.n + 1
|
||||
return { n = plugin.state.n }
|
||||
end
|
||||
return plugin
|
||||
`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
out, err := ps.Call("ondemand", "bump", map[string]interface{}{})
|
||||
if err != nil {
|
||||
t.Fatalf("call %d: %v", i, err)
|
||||
}
|
||||
if m, _ := out.(map[string]interface{}); m["n"] != float64(i) {
|
||||
t.Fatalf("call %d returned %v, want n=%d", i, out, i)
|
||||
}
|
||||
}
|
||||
// Close() waits for the final flush, so no timing is involved: anything the
|
||||
// calls did NOT mark dirty cannot appear here.
|
||||
ps.Close()
|
||||
|
||||
b, err := os.ReadFile(ps.stateFile("ondemand"))
|
||||
if err != nil {
|
||||
t.Fatalf("★ an exported call did not mark the plugin dirty: %v — a plugin "+
|
||||
"with no hooks can never persist its work", err)
|
||||
}
|
||||
var saved struct {
|
||||
State map[string]interface{} `json:"state"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &saved); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.State["n"] != float64(3) {
|
||||
t.Errorf("persisted n = %v after three exported calls, want 3", saved.State["n"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user