fix(plugins): 分片 VM 拿不到插件配置

页面探测走的是 fanout(分片)路径,结果写回走共享 VM。分片 VM 从同一份
源码启动,但没有 state 文件、没有插件身份,所以 PLUGIN.prices(配置,不是
累计状态)在分片里是空的。

对只读累计状态的插件无所谓;对按配置分支的插件就是致命的。OpenDesign
适配器读 prices.control_key,未播种时它对每个主机都得出「未配置」并报
skip,而同一个调用走共享 VM 是好的。也就是说:生产上死、所有调共享 VM 的
测试全绿。

pool 在每次取用时推送配置(不只是首次启动时),并把配置变化也带给池中
复用的 VM;推送是拷贝,分片拿不到共享表的引用。

这条不是在写代码时想到的,是端到端验证时撞出来的:单元测试全过,隔离实例
打真上游时 vela 报「未配置」,而 control_key 明明写进去了。

(cherry picked from commit 65075b6d44)
This commit is contained in:
JianFeeeee
2026-10-07 01:19:15 +08:00
parent 97a6cf9224
commit f042bbf4af
3 changed files with 230 additions and 0 deletions

View File

@ -1172,6 +1172,54 @@ func audienceAllows(want, role string) bool {
// upgrade would overwrite anyway.
type visibilityOverrides map[string]string
// seedShardConfig copies the shared plugin's `prices` table onto a shard VM.
//
// `prices` is CONFIGURATION, as opposed to `state` (accumulated history). A shard
// must not write state — its copy is thrown away — but it must be able to READ
// the operator's configuration, or a plugin that branches on its configuration
// behaves one way through Plugins.Call and another through ShardCall.
//
// Found by the OpenDesign adapter, which reads prices.control_key: without this, a
// fanout probe saw an empty table, concluded "not configured", and reported a
// skip for every host — while the identical call through the shared VM worked.
// The page uses the fanout path, so the feature would have been dead in
// production and alive in every test that called the shared VM.
//
// Called on every checkout (boot and reuse), so a configuration change reaches
// pooled VMs too. The push is a copy: shards get the same values, never a live
// reference to the shared table.
func (ps *Plugins) seedShardConfig(name string, w *worker) {
if ps == nil || w == nil || w.L == nil {
return
}
ps.mu.RLock()
p := ps.find(name)
ps.mu.RUnlock()
if p == nil {
return
}
p.mu.Lock()
if p.state == nil || p.state.L == nil || p.state.L == w.L {
p.mu.Unlock()
return
}
prices := snapshotField(p.state.L, "prices")
p.mu.Unlock()
if prices == nil {
return
}
L := w.L
L.SetTop(0)
defer L.SetTop(0)
L.GetGlobal(pluginGlobal)
if L.IsNil(-1) {
return
}
plug := L.GetTop()
pushGoValue(L, prices)
L.SetField(plug, "prices")
}
// SetPluginVisibility records an operator override for a plugin's audience. An
// empty value clears the override and restores the plugin's own default.
func (ps *Plugins) SetPluginVisibility(plugin, audience string) {