Skip to content

集群对端的元数据写入不失效本节点的 listCache / registry —— 收到广播的节点最长 30s 继续服务旧定义 #5109

Closed

Description

@os-zhuang

发现于 #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(同文件内已做对的那条路径,可作修法样板)。

Activity

  1. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    串行注记(PM 循环第 1 轮,会话 session_01Pbu27iNUfQCHeuS551Rqo7):本单本轮不派,与 #5108 同文件串行 —— #5108 的修法可能触碰 MetadataManager.list() 的降级日志面(packages/metadata/src/metadata-manager.ts),与本单的 attachClusterPubSub / listCache 失效修复同文件。#5108 落地后下轮即派,届时基于合并后的 main 重读 notifyWatchersLocal 现状(#5108 的派发词已禁止其触碰本单的面,预计无实质交叠)。


    Generated by Claude Code

  2. self-assigned this
    on Aug 4, 2026
  3. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮(串行解除:#5108→PR #5183 已合入 main,3133cda)
    会话:session_01Pbu27iNUfQCHeuS551Rqo7
    分支:claude/issue-5109-cluster-invalidate-list-cache
    Worktree:objectstack-issue-5109
    域:domain:engine
    文件面:packages/metadata/src/metadata-manager.ts(attachClusterPubSub / notifyWatchersLocal / listCache 失效一段)及本包测试、changeset(越界即停,报告说明)

    前提更新:issue 引用的 :1800-1833 已漂移(现 attachClusterPubSub 在 :2125 附近),且 #5183 今日改了同文件的 list()/loadMany() 降级缝(loaderReadFailureReported 一套)—— dev 基于合并后 main 工作,⛔ 不回改 #5183 落地的 loader/降级面。⛔ packages/spec/** 零改动。在飞不相交已核:#5038(objectql/service-automation)、#5134(driver-sql)与本面不交。


    Generated by Claude Code

  4. os-zhuang commented on Aug 4, 2026

    @os-zhuang
    ContributorAuthor

    验收:ACCEPT —— PR #5219(CI 24/24 全绿,转 ready 入队)

    会话:session_01Pbu27iNUfQCHeuS551Rqo7(PM 循环第 2 轮)

    落地内容:两条「外部写入」缝(repo watch 循环 applyRepoEvent、集群对端回放)收敛到同一个 invalidateForForeignWrite(type, name),原样提取 applyRepoEvent 的删而不预填形状。三个设计选择全部有测试钉住:(1) 删除不预填(registry 条目在 list() 里盖过 loader 同名项,只清 listCache 会让旧副本永活 —— 有专门用例);(2) 同步失效、先于延迟通知(setImmediate 只为消费方回调背压存在,失效是两次 Map.delete,推迟只会把 30s 的 bug 缩成一拍的 bug);(3) 无名事件只清清单缓存,不驱逐 registerInMemory 的代码态构件。issue 点名的「失效通知附带失效数据」矛盾消解:被叫醒的 watcher 回读 list() 拿到写后清单(有专门用例)。

    实况核验:draft→ready ✓ / Fixes 行 ✓ / 4 文件全在声明面内 ✓ / ⛔ 未回改 #5183 的 loader 降级面、packages/spec 零改动 ✓ / 7 例新回归中 6 例经 git stash 证明在 main 上是红的(真回归,不是环境巧合)/ 全包 433 + 下游 service-cluster 45 全绿 / tsc --noEmit 基线 92→92 净零新增 / cluster.mdx §6.2 只做使既有句子为真的增补。

    顺带发现处置:#5218(FS 监听路径同形缺陷)已由 dev 立卡,PM 分诊为 pm:blocked(等本 PR 合入后复用 invalidateForForeignWrite 样板,与 #5184 同字段建议串行)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions