Files
efi-kernel/文档/04-契约与调用.md
lou 53e7d1e5f2 底座文档 11 份 + 修掉"同锁调用互相排队、双双卡死"
文档/ (2026-09-16; 老板定调: 这是 agent 底座, 所以必须扎实, 现在越扎实以后开发越简单)
  00-索引           文档地图 + 30 秒概念速查 + 三条命令跑起来 + 事实源优先级
  01-快速上手       体检 -> 建库 -> 起内核 -> 起驱动 -> 收工, 全带实测输出; 第一次最易踩的四个坑
  02-写一个驱动     五分钟最小驱动 / 形态选择 / 能碰哪些表 / 汇报与调用两份模板 / 交付检查表
  03-命令手册       两层每条命令 + 日志选项 + 退出码约定 + --json 样例 + 日常十条
  04-契约与调用     一次调用的完整生命周期 / 六条仲裁 / 锁与按需拉起 / 排障表
  05-日志与排障     三条道怎么读 + "症状->判据->处置"总表 + 断电收尸语义
  06-架构与不变量   分层 / 14 条硬不变量 / 主流程表 / 双真相 / 为什么故意不做 / 已知薄弱点
  07-模块与接口     逐模块职责与公开接口 + "想改 X -> 动哪几处"连带清单
  08-数据模型       8 张表逐字段 (谁写谁读) + events.kind 字典 + 状态机 + 快照 + 排查 SQL
  09-扩展指南       六个配方 (加子命令/加字段/加表/加日志来源/加自测/改判定) + 同步清单
  10-验收与质量门   四道门 + 五份自测明细 + pyright 严格档 + 26 条已知坑总表 + 发布 checklist
  规矩: 不重复设计文档 / 每条命令实测过再写 (含 jq 表达式) / 代码>设计>文档 的事实源优先级 /
        改代码必须同步文档 (清单在 09 末尾) / 暂时没做到的事写成"已知边界"不含糊过去

修复: 同锁串行化原来是死的 (实测抓到的真缺陷)
  旧行为: db.领调用 只领 pending (waiting 没人再碰) + db.同锁在跑 把 waiting 也算"占着锁"
          -> 同一把锁上两条请求互相排队, 双双停在 waiting 谁也不跑 (实测 id 16/17);
             而 收权超时 只收 running -> 排队连超时都没有 = 死锁
  修法:   ① db.领调用 的 SQL 改 state IN ('pending','waiting') -- 每轮把排队的领回来重判, 锁一空就推进
          ② db.同锁在跑 只认 state='running' (排队的还没拿到锁, 不挡人)
          ③ 内核.转发调用 waiting 分支补 deadline (排队也立期限); 内核.收权超时 遍历 running + waiting
          ④ 抽出 内核.期限文本() 统一算 deadline
  实测:   两条同锁调用串行跑完 (19.started_at == 18.finished_at); 排队者超时被收权 (events 有记录)
  回归:   自测db.py 调用组 +4 条断言 (waiting 不算占着锁 / waiting 会被重新领 / ...);
          去掉一条依赖生产库全局计数的脆弱断言

其它: 内核 与 引导器 的 用法() 末尾加文档指引
验收: uvx pyright 0 errors / 0 warnings; 五份自测全过 (进程/内核 58/配置/db/日志 86);
      试跑引导器.py PASS 11 / FAIL 0 / 残留无; 残留进程 0
2026-09-16 21:13:31 +08:00

9.0 KiB
Raw Permalink Blame History

04 · 契约与调用(驱动之间怎么"牵线")

核心口径:驱动之间零耦合 —— 不 import 对方、不互相调用、配置里也不写对方的名字。 只声明"我要什么 / 我产出什么"(契约名),谁给、什么顺序、怎么送达全归内核。 这里讲的是它;判定表原文在 设计/02-内核设计.md 第 2 节末与 设计/01-驱动规范.md 第 3 节。

1. 契约 = 中立的能力名

样板:心跳       ← 好: 说的是"一种能力", 换谁提供都不影响消费方
样板常驻        ← 坏: 这是驱动名, 消费方就"认识"对方了 (一旦换名字就全断)

命名建议 域:能力(冒号当分隔,纯文本,大小写敏感)。契约名不是驱动名,也不是文件路径 —— 它只是内核手里那张"谁提供什么"的对照表里的一个键。

字段 谁声明 含义
provides 提供方 产出什么能力(可以多个)
needs 消费方 需要什么能力(可以多个)

同一个契约被两个驱动声明 provides → 按驱动名排序取先的,并记一条扫描警告(确定性,不靠运气)。

2. 一次调用的完整生命周期

消费方                      calls 表                        内核(常驻)                  提供方
  │                            │                               │                        │
  ├─ INSERT (want=契约名) ────►│ pending                       │                        │
  │                            │ ◄── 领调用 (pending/waiting→waiting)                   │
  │                            │ ◄── 六条仲裁                                          │
  │                            │                               ├─ 没在跑? 按需拉起 ───►│
  │                            │  running (provider/deadline)  │                        │
  │                            │ ◄────────── pg_notify driver_<提供方> ─────────────────┤
  │                            │                               │              干活      │
  │ ◄── 轮询 id 直到有结论 ────│ done / failed                 │ ◄── UPDATE result ─────┤
  │                            │                               │                        │
  │  另外三种结论: denied (被拒) / timeout (内核收权) / waiting (排队中)                  │

calls.state 取值与含义:

state 谁写的 含义
pending 消费方 INSERT 刚发出来,等内核看见
waiting 内核 正在仲裁中,或在锁上排队(每轮会被重新领回来重判,锁空即推进)
running 内核 已转发给提供方(provider 有值,deadline 已立)
done / failed 提供方回填 干完了(结果在 result/ 干不了(原因在 error
denied 内核 被仲裁拒(越权 / 成环 / 没人提供 / 提供方起不来)
timeout 内核 过了 deadline,内核收权(runningwaiting 都会超时)

3. 仲裁六条(内核拿到请求时依次判什么)

# 规则 不过时 怎么修
1 越权:只能要自己 needs 里声明过的契约 denied越权: <驱动> 的 needs 里没有 <契约> 配置.efi.jsonneeds 里补上
2 成环:要的契约或其提供方已在这次调用的链上 denied调用链成环: A -> B -> A 检查依赖方向(扫描期就能挡掉静态环)
3 无人提供 denied契约没人提供: <契约> 要么写提供方,要么去掉这个 needs
4 提供方没在跑 → 按需拉起 拉不起来才 denied提供方 X 起不来: … 看提供方的日志(它自己起不来)
5 同一 lock_key 串行化(先到先执行) 排队:锁 X 上已有调用在跑, 排队waiting 正常现象;等前一条干完会自动推进
6 超时收权deadline 到期) 内核标 timeout 并释放锁 看提供方为什么慢;调用默认时限秒 = 60

链(chain)是消费方自己带的路书args = {"chain": ["驱动A"]},转发时内核把它当"这次请求已经走过的路"。 判环只看两件事:要的契约匹配出来的提供方是否已在链上 —— 不能拿发起方自己判(它就是链尾, 那样第一次调用也会被拒;这个坑 2026-09-16 踩过)。

4. 锁(lock_key):同一份数据不许两个人同时写

  • lock_key消费方给(建议直接用契约名)。为空 = 不参与串行化。
  • 判定只认 state='running'拿到锁在干活的那条才挡人;排队中的(waiting)不挡人。
  • 排队者每轮被重新领回来重判 → 前一条 done 后自动推进(实测:两条同锁请求 18 19 串行跑完,19.started_at = 18.finished_at)。
  • 排队也有期限:第一次排队时立 deadline,排到超时会被收权(timeout),不会永远等。

2026-09-16 修掉的真缺陷:以前"只领 pending" + "waiting 也算占锁",导致两条同锁请求 互相排队、双双卡在 waiting 谁也不跑。改了两处 SQLdb.领调用 / db.同锁在跑+ 排队补 deadline,回归测试在 内核/自测db.py 的调用组("waiting 不算占着锁"/"waiting 会被重新领")。

5. 按需拉起:消费方不用管提供方在不在跑

内核在仲裁第 4 步发现提供方没在跑,会当场把它拉起来(记一条 被调用 N 按需拉起 的事件):

提供方 mode 内核怎么处理
resident 拉起(它自己 LISTEN driver_<名>,等内核 pg_notify 转发)
oneshot 拉起 → 它跑一遍 → 自己退(不用 LISTEN

实测(设计/02 里的活体验收):只起消费器,内核自动把提供方拉起来,calls 回填 done

调用 5: 样例消费器 要 样板:心跳 -> 转发给 样板常驻
样板常驻 起来了 pid=… (被调用 5 按需拉起)

6. 驱动侧两份模板

发请求(消费方) —— 完整可跑版 驱动/样例消费器/请求.py

# ① 插一行 (want 写契约名; chain 带上自己, 让内核能挡环)
游标.execute(
    "INSERT INTO calls (caller, want, args, lock_key) VALUES (%s, %s, %s::jsonb, %s) RETURNING id",
    ("我的驱动名", "样板:心跳", json.dumps({"chain": ["我的驱动名"]}, ensure_ascii=False), "样板:心跳"),
)
调用id = int(游标.fetchone()[0])

# ② 轮询直到有结论 (等不到要如实说"等不到", 别无限等)
#    SELECT state, provider, result, error FROM calls WHERE id = %s
#    state ∈ {done, failed, denied, timeout} 就是有结论了

# ③ result 里取数据

接活(提供方,resident —— 完整可跑版 驱动/样板常驻/心跳.py

连接.execute("LISTEN driver_我的驱动名")     # 通道名跟"我是谁"有关, 跟谁在调我无关
# 主循环里: while 连接.notifies: 通知 = 连接.notifies.pop(0)
#   调用id = int(通知.payload)               # 内核 pg_notify 的载荷就是 calls.id
#   干活 → UPDATE calls SET state='done', result=%s::jsonb, finished_at=now() WHERE id=%s
#   **只许改自己这条**: 不改别人的行, 也不去查"谁在调我"

驱动不许直接调用另一个驱动,也不许知道对方是谁 —— 这是"打不起来架"的全部秘密。

7. 排障表(调用相关)

症状 判据 处置
请求一直 pending 常驻内核没在跑 python3 UEFI.boot.py 内核 状态;没跑就 内核 启动 --守护
denied: 越权 needs 没声明 配置里补 needs重启 <驱动>
denied: 契约没人提供 没有驱动的 provides 对上 列表/状态 看那个驱动是不是 无效(原因在它 error 里)
denied: 提供方 X 起不来 提供方自己坏 内核/内核.py 日志 X -n 50,看它临死说了什么
卡在 waiting 同一 lock_key 上还有 running 正常排队;若对方真卡死,等 deadline 到点内核收权(默认 60s
timeout 提供方干太久 查提供方;需要更长的时限就调 内核/内核.py 里的 调用默认时限秒
回填了结果但消费方看不到 消费方在轮询别的 id,或状态不是终态 psql … SELECT * FROM calls WHERE id=… 看真实状态

8. 本版不做(边界,别以为是 bug)

  • 没有优先级 / 公平性:锁上按 id 先到先得,没有权重、没有抢占。
  • 没有推送回调:消费方轮询 calls 行(pg_notify 只用于内核 → 提供方那一段)。
  • 没有结果大小限制result 是 jsonb,大对象该走"提供方写自己的表 + result 只放引用"。
  • 没有跨库调用:一张 calls 表、一个 PG 库。跨库需求 = 换架构,不在本版。