CORDISDSH 的心脏第 05 章 · 思想二 · 插座全书目录术语表读论文原文 ↗
第五章 · 思想二 · 响应式反效应

缺了就歇着,
齐了就上岗

小周半夜被叫起来重启,是因为插件把「数据库一定在」当成了理所当然。论文的第二个思想:依赖不写死,写成一张清单,让运行时盯着。

约 8 分钟 · 上一章 04 思想一 · 回执 · 下一章 06 思想三 · 一扇门

题图隔壁收摊你歇业,隔壁回来你开张。

半夜那十几秒

小周的第二个坑:数据库抽风十几秒,提醒插件拿不到连接,崩了;管理插件跟着崩。

老写法的问题在于,依赖是「启动那一刻接好就不管了」。程序启动时把线连上,然后默认它永远在。线一断,所有用到它的地方同时出错。要防住,只能在每个插件里写一堆「先看看在不在、不在就重试」的代码。每个作者写一遍,写法还各不一样。

进门先交一张需求清单

论文的第二个思想叫响应式反效应。拆开说:组件进门时先交一张清单(Cordis 里叫依赖声明),写明「我需要什么」;运行时替它盯着环境。

环境每变一次(有东西上线、下线、换了),运行时就拿每一份清单对照这次变化,分成三类 p.6:

这次变化对它来说 运行时怎么做
从缺到齐 激活:让它上岗
从齐到缺 停用:让它歇着,并按回执撕干净
和它无关 不理,它甚至不会察觉
老写法自己盯着
function sendReminder() {if (!db || !db.connected) {  retryLater()    // 每个插件都写一遍  return}  db.query(...)}
每个用到数据库的地方,都要先怀疑一次它在不在。
Cordis 写法(示意)交清单
export const inject = ['database'] export function apply(ctx) {  // 能走到这里,数据库一定在  ctx.on('message', () => {    ctx.database.query(...)  })}
清单不齐,apply 根本不会被调用;数据库走了,这个插件会被自动停下、撤干净。
演示 5.1 · 可以点断供即歇业,来电即开张
    数据库没有
    还没有谁提供
    提醒插件未装
    需求清单:数据库
    统计面板未装
    需求清单:提醒插件
    天气插件未装
    需求清单:(空)
    运行时的分类记录
    (环境还没变过)
    旁白提醒插件要用数据库;统计面板要读提醒插件的数据;天气插件谁都不靠。三个插件都已经写好了需求清单。按「开始」,先在没有数据库的时候把它们装上。
    按主按钮一拍一拍往下走

    两个思想,严丝合缝

    这里有个很漂亮的地方。「数据库上线」这件事本身,就是提供者做的一个改动:在环境里登记「我能提供数据库」。按上一章的规矩,这个登记也有回执。

    所以数据库下线,就是撕掉这张登记的回执;撕的那一刻,所有清单上写着「数据库」的组件都会收到通知。第一个思想管「改动怎么撤」,第二个思想管「撤了以后谁受影响」,两者共用同一套账,不会出现「数据库已经走了,却没人通知插件」的空档。

    还有顺序。数据库要走时,会先挂出停工告示,等依赖它的组件一个个撤完,自己才离场。谁先走、谁后走,由依赖关系决定,不由谁写在前面决定。这套流程的细节在第 07 章。

    认人,不认货

    演示的第三拍可能让你意外:换成备用库,连接地址一模一样,为什么提醒插件还要重新上岗?

    图 5.1 · 换了一匹马

    不要骑在一匹正被牵走的马上

    每个提供者上岗时,会领一个永不复用的编号。系统认编号不认内容:编号变了,就是换了人,哪怕两位给的东西一模一样 p.63。

    为什么这么较真?因为「长得一样」不等于「是同一个」。旧连接可能正在关闭,还握着旧的缓存。依赖它的人如果不重新接,就像骑在一匹正被牵走的马上。

    同一个名字,指向不同的东西

    清单上写的是名字,比如「数据库」。论文给这个名字留了两个很实用的口子。

    隔离:同一个名字,在不同的子环境里各指向一份私有的东西。两个租户的插件都写着「我要数据库」,各自拿到的是自己那一份,看不见对方的。多租户、测试环境、多个 agent 并排跑,都靠它分开。

    拦截:在「读依赖」这个动作外面叠一层规则,比如限流、审计、换个实现。提供方和使用方的代码都不用改,改规则也不用重启。

    图 5.2 · 门口加一道闸

    规则加在路上,不加在人身上

    拦截像商场门口的安检:进出的人不用改变任何习惯,规则只加在必经之路上。今天要限流,明天要记日志,都是换一道闸的事。

    带走这一句

    依赖不写死,写成清单:缺了就歇着,齐了就上岗,换了人就重新接。

    请改造这个项目里模块之间的依赖关系:
    1. 每个模块在开头声明它依赖哪些服务(名字列表),不要在代码中途去「查一下在不在」;
    2. 由一个统一的加载器负责:依赖齐全才启动模块;依赖消失就停止模块,并执行它的全部清理;
    3. 依赖的提供者被替换时(即使配置相同),依赖它的模块要重新启动;
    4. 列出原来那些「检查是否存在 / 失败重试」的代码,说明改造后哪些可以删掉。