发现于 #5089(#5040 E2 端点匹配器)实现期,不在该单范围内,故单独立卡。
现象
packages/metadata/src/metadata-manager.ts attachClusterPubSub()(:1800-1833)在收到对端的 metadata.changed 广播时,只做一件事:
setImmediate(() => {
this.notifyWatchersLocal(p.type, p.event);
});
notifyWatchersLocal(:1770-)只把事件派发给 watchCallbacks。它不碰 this.registry,也不碰 this.listCache。
对比本节点自己的写入路径 —— register() / unregister() / registerInMemory() 都先 invalidateListCache(type) 再 notifyWatchers(...);applyRepoEvent()(:1704-)更是显式地把 registry 条目删掉再 listCache.delete(type) 才通知。也就是说:本地写会失效缓存,对端写只会通知 watcher。
后果:节点 A 改了一条 view/permission/flow,节点 B 收到广播,B 的 watcher 们(ObjectQL SchemaRegistry 桥、HMR SSE)确实被叫醒了,但 B 上任何走 list(type) 的读在 LIST_CACHE_TTL_MS = 30_000 窗口内继续返回改动前的清单。被叫醒的 watcher 如果自己回头调 list() 重新拉取,拉到的还是旧的 —— 失效通知与失效数据自相矛盾。
为什么值得单独修
- 这与
cluster-semantics.mdx §5 声称的语义不符:该通道的用途原文就是 "consumed by peers to invalidate their local caches"(见 ClusterMetadataChangedPayload 的注释),而实现只做了通知、没做失效;
- 单机部署完全无感,只有多节点才暴露,属于「看着正常、其实供旧数据」那一类;
- 修法看起来很小(在
notifyWatchersLocal 之前补 registry 条目删除 + listCache.delete(type),即复用 applyRepoEvent 已有的那几行),但要不要连 registry 一起删牵涉语义选择(applyRepoEvent 的注释解释了它为什么选择「删而不预填」),所以不该顺手在别的单里带一刀。
复现思路
两个 MetadataManager 接同一个 IPubSub(memory 驱动即可),各自 attachClusterPubSub(pubsub, nodeId);在 A 上 list('view') 预热 B 的缓存做不到 —— 直接在 B 上先 list('view') 预热,然后 A 上 register('view', ...),再在 B 上 list('view'):返回的是预热时的旧清单。
关联
cluster-semantics.mdx §5、#5089(发现处)、applyRepoEvent(同文件内已做对的那条路径,可作修法样板)。
发现于 #5089(#5040 E2 端点匹配器)实现期,不在该单范围内,故单独立卡。
现象
packages/metadata/src/metadata-manager.tsattachClusterPubSub()(:1800-1833)在收到对端的metadata.changed广播时,只做一件事:notifyWatchersLocal(:1770-)只把事件派发给watchCallbacks。它不碰this.registry,也不碰this.listCache。对比本节点自己的写入路径 ——
register()/unregister()/registerInMemory()都先invalidateListCache(type)再notifyWatchers(...);applyRepoEvent()(:1704-)更是显式地把 registry 条目删掉再listCache.delete(type)才通知。也就是说:本地写会失效缓存,对端写只会通知 watcher。后果:节点 A 改了一条 view/permission/flow,节点 B 收到广播,B 的 watcher 们(ObjectQL SchemaRegistry 桥、HMR SSE)确实被叫醒了,但 B 上任何走
list(type)的读在LIST_CACHE_TTL_MS = 30_000窗口内继续返回改动前的清单。被叫醒的 watcher 如果自己回头调list()重新拉取,拉到的还是旧的 —— 失效通知与失效数据自相矛盾。为什么值得单独修
cluster-semantics.mdx§5 声称的语义不符:该通道的用途原文就是 "consumed by peers to invalidate their local caches"(见ClusterMetadataChangedPayload的注释),而实现只做了通知、没做失效;notifyWatchersLocal之前补 registry 条目删除 +listCache.delete(type),即复用applyRepoEvent已有的那几行),但要不要连registry一起删牵涉语义选择(applyRepoEvent的注释解释了它为什么选择「删而不预填」),所以不该顺手在别的单里带一刀。复现思路
两个
MetadataManager接同一个IPubSub(memory 驱动即可),各自attachClusterPubSub(pubsub, nodeId);在 A 上list('view')预热 B 的缓存做不到 —— 直接在 B 上先list('view')预热,然后 A 上register('view', ...),再在 B 上list('view'):返回的是预热时的旧清单。关联
cluster-semantics.mdx§5、#5089(发现处)、applyRepoEvent(同文件内已做对的那条路径,可作修法样板)。