fix(metadata): FS 监听改动同样失效本节点的 listCache/registry (#5218) - #5229
Merged
Conversation
#5218) `NodeMetadataManager.handleFileEvent()` did two things when chokidar reported `add` / `change` / `unlink`: re-`load()` the file and `notifyWatchers()`. It touched neither `listCache` nor `registry` — and `load()` is a pure read (it delegates to `loadDiagnosed`, which only walks the loaders), so both caches kept the pre-change state. The manager's two read surfaces then contradicted each other for up to LIST_CACHE_TTL_MS (30s) after editing `rootDir/view/x.json`: `get()` returned the new definition because it falls through to the FilesystemLoader, while `list()` — REST `/api/v1/metadata/:type`, the Studio left rail, `listViews()` — kept serving the pre-change set. The HMR/SSE consumers the event woke answer it by re-reading through `list()`, so the wake-up handed back precisely the stale data it was announcing. Same defect shape as #5109 (a cluster peer's write) with a different trigger, so this reuses that fix's helper rather than re-deriving it: `invalidateForForeignWrite(type, name)`, widened `private` -> `protected`. A file event is a foreign write on the definition that matters — it did not come through this manager's write API, so nothing refreshed the caches on its behalf, and delete-not-prefill fits exactly (falling through to the loader IS the file's truth). Two constraints kept in line with every other write path in the base class: invalidate BEFORE announcing (`register` / `unregister` / `applyRepoEvent` / the cluster subscriber all do), so a watcher can never observe the event and the pre-event cache together; and drop the registry entry too, not just the list cache — FS-loaded items never enter the registry, but a same-named entry previously written by `register()` / `registerInMemory()` SHADOWS the loader in both `get()` and `list()`, and dropping the list cache alone would leave that stale copy answering forever. `type === 'api'` is unchanged in behaviour: the endpoint index was already covered on this path by #5089's `subscribe('api', ...)` seam. The fix connects the `invalidateListCache` seam too, making the two symmetric; the overlap is free because `EndpointMatcher.invalidate()` is two assignments to `undefined`. Hit surface is development-time: `MetadataPlugin` defaults to `watch: true`, forced off under `bootstrap: 'artifact-only'`, and `standalone-stack` passes `watch: false`. Tests drive `handleFileEvent` with synthetic events over real files, a real tmpdir and the real default FilesystemLoader — `startWatching` polls at `interval: 1000`, so a real watcher per case would be seconds of wait for no added coverage. One end-to-end case uses a real chokidar watcher to pin that its callbacks actually reach the handler. 9 of the 11 fail without the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 11:37
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5218
问题
NodeMetadataManager.handleFileEvent()在 chokidar 报告add/change/unlink之后只做两件事:重新load()一次文件内容,然后notifyWatchers()。它既不碰listCache也不碰registry—— 而load()是纯读路径(它委托给loadDiagnosed,后者只遍历 loader),两个缓存都不写。于是同一个 manager 的两个读接口互相矛盾。手改
rootDir/view/x.json之后:get(type, name)是新的 —— 它穿透到FilesystemLoader;list(type)在LIST_CACHE_TTL_MS(30 秒)窗口内继续返回改动前的清单 —— REST/api/v1/metadata/:type、Studio 左栏、listViews()等一切走list()的读。更糟的是被这次事件叫醒的消费者(Studio HMR/SSE、ObjectQL SchemaRegistry 桥)正是通过回头拉
list()来响应的,于是这次唤醒递回了它自己刚刚宣告已失效的那份数据。修法
与 #5109(集群对端那条同形缺陷,PR #5219 /
533a0a4)是同一形状、不同触发源,因此复用该修复落地的样板而不是另起一套:invalidateForForeignWrite(type, name),可见性由private放宽为protected。文件改动正是「不是经由本 manager 写接口发生的写入」这个判据下的 foreign write —— 没有任何东西替它刷新过缓存;delete-而非-预填 的语义也正好对上:穿透回 loader 读到的就是文件的真相。
两点与基类其余写路径一致的约束:
register/unregister/applyRepoEvent/ 集群订阅者都是这个次序,使 watcher 不可能同时观察到事件与事件前的缓存(fix(metadata): 集群对端的元数据写入现在会失效本节点的 listCache / registry (#5109) #5219 钉的同一条纪律)。register()/registerInMemory()写过时,它在get()和list()中都会遮蔽 loader(两者都是 registry 优先合并),只删列表缓存会让那份陈旧副本一直应答下去。关于 PM 提出的「若复用不成立则退回只放宽
invalidateListCache」已核,复用成立,无需退回。逐个检查了
registerInMemory的调用方(default-datasource-plugin.ts的default数据源、app-plugin.ts的 code-defined 数据源与 stack 声明的安全元数据),它们只会在rootDir/< type >/< name >.< ext >确实存在并触发事件时才被删中一个同名条目;而单名删除(不是整个 type store)正是该 helper 的既有契约,registerInMemory产物按名字被保护。回归测试keeps in-memory-only entries of OTHER names intact钉住了这一点。反过来,只放宽
invalidateListCache是不够的:遮蔽场景下get()仍会返回陈旧的 registry 副本(drops a shadowing registry entry so reads fall through to the file用例覆盖)。关于 PM 提出的 EndpointMatcher 不对称性
已核,并且确认它不是缺陷。
type === 'api'的文件事件此前已经由 #5089 装的subscribe('api', …)那条缝失效了端点索引;空的是invalidateListCache那条缝。本次改动把第二条缝接上,两条缝对称。实测:
api那个用例在改动前后都通过(移除本修复后 11 个用例失败 9 个,该用例是通过的 2 个之一),所以它是一道 guard 而不是回归钉 —— 测试注释已如实写明。重复失效幂等:EndpointMatcher.invalidate()是两次赋undefined(与 #5089 注释所述一致)。命中面
主要是开发期,不是生产多节点。
MetadataPlugin默认watch: true,仅在bootstrap: 'artifact-only'下被强制关闭(plugin.ts:240-249);standalone-stack.ts:341 显式传watch: false。即 artifact 模式的os dev与 standalone 不受影响,非 artifact 的默认MetadataPlugin装配受影响(与单内「触发前提」一节一致,已按现网main复核)。测试
新增
packages/metadata/src/node-metadata-manager-fs-invalidation.test.ts(11 个用例)。驱动方式:以合成事件直接调用
handleFileEvent,其余全部是真的 —— 真临时目录、真文件、默认的真FilesystemLoader、真list()/get()/matchEndpoint读路径。理由是startWatching以interval: 1000轮询,每个用例都挂真 watcher 只会换来数秒等待而不增加对被测接缝的覆盖。合成事件唯一证明不了的事(chokidar 的回调确实抵达handleFileEvent)由末尾一个真 watcher 的端到端用例钉住。该端到端用例需要先等 chokidar 的
ready:ignoreInitial: true下,在初次扫描完成前创建的文件会被并入基线而永不触发add—— 不等就是一个以 30 秒超时形式暴露的竞态(初版即如此,已修)。移除本修复后 11 个用例失败 9 个;通过的 2 个是负向对照(非元数据路径)与上述
apiguard。npx tsc --noEmit(本包无typecheck脚本,按类型检查覆盖率账本计 DEBT):92 条报错,全部落在既有的其它测试文件(TS2835/TS7006 债务),本 PR 触碰的三个文件贡献 0 条 —— 与origin/main无增量。顺带发现(未在本 PR 修)
已另立 #5228(
finding标签,休眠类):handleFileEvent的try/catch对 loader 读/解析失败不可达 ——load()委托的loadDiagnosed按 ADR-0110 D3 把 loader 异常吞成{ data: null, degraded: true },所以坏文件被当作data: null广播出去,与「合法地什么都没有」同形。目前仓内无消费者读该data(ObjectQL 订阅者回头get()重读),故今天无人踩到。是写第 8 个用例时按「会提前返回」写、实测失败才发现的;该用例已按真实行为改写并注明。🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7