Skip to content

hostImporter CJS/ESM 双构建实例错位:OS_CLUSTER_DRIVER=redis 注册进 CJS registry、Runtime 查 ESM registry → 拒启,且 serve 静默吞错 #13365

Description

@baozhoutao

现象

宿主 app 声明了 @objectstack/service-cluster / @objectstack/service-cluster-redis(#4719 契约满足)、设 OS_CLUSTER_DRIVER=redis 走 os serve,boot 拒启:

✗ Cluster driver "redis" is not registered. Did you forget to import @objectstack/service-cluster-redis or call registerClusterDriver()? See content/docs/kernel/cluster.mdx §6.

而手动在同一宿主目录 import('@objectstack/service-cluster-redis') 完全正常(导出齐全)。serve 侧没有任何线索:装载失败被空 catch 吞掉。

  • 实测环境:平台 17.2.0(objectos-ee:4.1.1 镜像,builtAt 2026-08-24),宿主 app 为 os-tianshun-ehr 的 EE 薄扩展镜像(经 file: 声明 + 符号链接指向镜像内 workspace 包)。
  • 复现与临时绕法收录在 steedos-labs/os-project-titanwind-ehr 的 deploy-ee/README.md 与 deploy-ee/cluster-preload.mjs(PR steedos-labs/os-project-titanwind-ehr#1880)。

根因链(逐环节核实过)

  1. serve(packages/cli/src/commands/serve.ts,OS_CLUSTER_DRIVER 分支)经 importFromHost(createHostRequire 认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制 #4719 宿主解析器)装载 @objectstack/service-cluster-${driver}。

  2. createHostImporter(packages/types/src/node.ts)对已声明的包走 hostRequire.resolve(pkg) —— CJS 解析,命中 exports 的 "require" 条件:

    hr.resolve('@objectstack/service-cluster-redis')
    → …/service-cluster-redis/dist/index.cjs        ← CJS 构建
    hr.resolve('@objectstack/service-cluster')
    → …/service-cluster/dist/index.cjs
    
  3. 于是 registerClusterDriver('redis', …)(service-cluster-redis/src/index.ts 末尾的模块副作用)注册进的是 service-cluster CJS 构建实例的 driverRegistry —— 这是个模块级 const Map(packages/services/service-cluster/src/cluster.ts),不挂 globalThis。

  4. Runtime 的 defineCluster 从 ESM 上下文 bare import @objectstack/service-cluster → ESM 构建实例(dist/index.js)→ 另一张空 Map → 抛 "not registered"。

  5. 加重:serve 对该 import 是空 catch { /* may already be registered by the loaded config */ } —— 真实失败模式(装载成功但注册进错实例/或解析失败)完全静默,操作者只能看到第 4 步的结果性报错,与真实原因相隔两层。

一句话:同一个包的 CJS 与 ESM 双构建各持一份 registry,注册与查找不在同一张 Map 上。

最小复现

任意宿主 app(package.json 声明上述两包并可解析),OS_CLUSTER_DRIVER=redis OS_REDIS_URL=redis://… os serve → 拒启如上。

项目侧临时绕法(可作修复验证参照)

入口改 node --import cluster-preload.mjs,预载脚本在宿主目录以纯 ESM await import('@objectstack/service-cluster-redis') 一次 —— Node 默认 realpath 到与 Runtime 相同的 ESM 实例,注册即生效。实测 3 副本集群恢复正常(redis fence 锁选主、跨副本协调均工作)。

建议修复方向(任一即可,前两者可叠加)

  1. driverRegistry(以及同类模块级单例,如 multi-node gate 的注册表)挂 globalThis 兜底,双构建实例共享一张表;
  2. createHostImporter 对已声明包在 ESM 上下文优先 import 条件解析(或 resolve 后按 import 条件重定向),避免 CJS/ESM 双实例;
  3. serve 的集群驱动装载不再静默吞错:至少把失败原因降级为 WARN 打出来 —— "may already be registered by the loaded config" 这个假设应该验证(查 registry)而不是假定。

波及面

不止 cluster:凡经 hostImporter 装载、靠模块副作用注册进模块级单例的包,双构建下都可能注册错位。已另立 cloud 侧 issue 追多节点授权门(registerMultiNodeGate)的同型错位与接线缺口(编号见评论/引用)。

Activity

  1. baozhoutao commented on Aug 30, 2026

    @baozhoutao
    ContributorAuthor

    cloud 侧连带缺口(多节点授权门 registerMultiNodeGate 的同型双实例错位 + 只在 EE config 接线导致薄扩展/制品路线不强制)已立:objectstack-ai/cloud#1752

  2. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊 → domain:cli · p1 · bug。跨域单,本座位指定 domain:cli 为认领车道。

    锚定。 三个候选修复点分属两个域:② createHostImporter(packages/types/src/node.ts)与 ③ serve 的静默 catch(packages/cli/src/commands/serve.ts)落 domain:cli;① driverRegistry 挂 globalThis(packages/services/service-cluster/src/cluster.ts)落 domain:services。根因是 ② —— 卡片自己也说了波及面「不止 cluster:凡经 hostImporter 装载、靠模块副作用注册进模块级单例的包」——所以主车道按根因落点定为 domain:cli,① 作为跨域半边由该车道 PM 在认领评论里申报完整文件面。

    在 origin/main 上复核了 ③,卡片属实且比它写的更明确:

    packages/cli/src/commands/serve.ts
    :2469   // Same host-anchored resolution as the gate above — the shipped
    :2470   // drivers (`-redis`, `-postgres`, …) are app-declared too. The catch
    :2471   // stays deliberately silent: the driver may already have been
    :2472   // registered by the loaded config, and an absent driver is a
    :2473   // documented fall-back to the in-memory cluster, not a boot failure.
    :2474   try { await importFromHost(`@objectstack/service-cluster-${__clusterDriver}`); }
    :2475   catch { /* may already be registered by the loaded config */ }
    

    这条 catch 是有意的,且注释写下了它的两条理由。卡片的第 ③ 条建议正中要害:两条理由都是假设而不是读数 —— 「可能已经被 config 注册过了」是可以查 registry 证实的,「驱动缺失是文档化的降级」也是可以区分的。派单时请把这一点写进去:修复不是「去掉 catch」,是把假设换成读数(import 后查 registry;查不到才决定是降级还是报错)。⛔ 不要改成直接抛 —— 缺驱动降级到 in-memory 是文档化行为,直接抛会把一个可用部署变成拒启。

    另一处历史读数:serve.ts:711 的文档块已经记过同一症状(OS_CLUSTER_DRIVER=redis 镜像启动时 Cannot find package)。派单前读那段,它可能已经排除掉一部分假设。

    ⚠️ 与 #13463 的关系:兄弟,不是重复。 #13463 说的是 serve.ts:2363 的空 catch 吞掉 'undeclared' 宿主导入分类;本卡是 :2474 的空 catch 吞掉集群驱动装载失败。两条不同的 catch,同一种病。两张卡都留着,但同一个 dev 连着做最省 —— 派单时请写明这个配对。

    定级 p1。 现场报告(os-tianshun-ehr EE 薄扩展镜像,平台 17.2.0,builtAt 2026-08-24),后果是三副本集群拒启,且真实原因与报错相隔两层。有绕法(node --import cluster-preload.mjs,已实测恢复),所以不是 p0;但绕法要求客户改入口,不是配置项。

    cloud 侧连带缺口已由报告方立在 objectstack-ai/cloud#1752(registerMultiNodeGate 同型双实例错位)。那是 cloud 座位的卡,本卡不代管;若两边同批修,请走跨座位路径而不是在一个 PR 里跨仓。


    Generated by Claude Code

  3. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊更正 —— 本卡与 #13330 是同一条缺陷,合并到 #13330(正卡),本卡作为重复关闭。

    我上一条评论把本卡定为 domain:cli p1 时还没读到 #13330。两张卡描述的是同一件事:同一个 importFromHost 接缝、同一次 CJS/ESM 双构建实例错位、同一条 serve.ts 静默 catch、同一个 Cluster driver "redis" is not registered 症状,同一位报告方。#13330 早 3 小时 15 分立卡,且带决定性读数 —— 同进程内同时探两个实例,ESM 侧 REGISTERED / CJS 侧 NOT registered —— 所以以它为正卡。

    本卡独有的两项内容已原样带到 #13330 上,不会丢:

    1. node --import cluster-preload.mjs 预载绕法(已实测 3 副本恢复,redis fence 选主与跨副本协调均工作),以及它与 cli: serve's cluster-driver load registers into the CJS registry while the ESM Runtime reads the ESM one — OS_CLUSTER_DRIVER=redis silently downgrades to "not registered" (post-#10645) #13330 那条 objectstack.config.ts 加一行 import 的适用差别;
    2. cloud 侧连带缺口 objectstack-ai/cloud#1752(registerMultiNodeGate 同型错位)的指针。

    上一条评论里关于「⛔ 不要把 catch 改成直接抛」「把假设换成读数」「serve.ts:711 有历史读数」「与 #13463 是兄弟不是重复」这几点也一并写进了 #13330 的派单说明。

    请到 #13330 跟进。 若认为两张卡实际是不同缺陷(例如你在两次复现里看到的失败点确实不同),说一声,我重开本卡并把两者的差别写清楚 —— 合并是我按现有证据下的判断,不是终局。


    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

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions