From 35513d00f527138c3224e124ca461f5b979e7c53 Mon Sep 17 00:00:00 2001 From: sdzx-1 Date: Thu, 19 Feb 2026 16:31:52 +0800 Subject: [PATCH] =?UTF-8?q?new=20post:=20Troupe=20=E2=80=93=20A=20Determin?= =?UTF-8?q?istic=20Distributed=20Protocol=20Composition=20Framework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/post/2026-02-19-troupe-1.smd | 126 +++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 content/post/2026-02-19-troupe-1.smd diff --git a/content/post/2026-02-19-troupe-1.smd b/content/post/2026-02-19-troupe-1.smd new file mode 100644 index 0000000..493897d --- /dev/null +++ b/content/post/2026-02-19-troupe-1.smd @@ -0,0 +1,126 @@ +--- +.title = "Troupe —— 确定性分布式协议组合框架", +.date = @date("2026-02-19T16:00:00+0800"), +.author = "sdzx", +.layout = "post.shtml", +.draft = false, +.custom = { + .math = false, // 如果你要用到数学公式,请 设置 math 为 true; 否则可以忽略 + .mermaid = false, // 如果你要用到 mermaid 图表,请设置 mermaid 为 true; 否则可以忽略 +}, +--- + +# Troupe —— 确定性分布式协议组合框架 + +[Troupe](https://github.com/sdzx-1/troupe) 是一个基于 Zig 语言类型系统的分布式协议构造库。它的核心理念是 **用类型的确定性来对抗通信的不确定性**:将协议建模为完全确定的状态机,通过编译期验证保证正确性,并将所有通信不可靠性(延迟、丢包、乱序)隔离在可替换的通道层之下。最终,开发者可以像编写单机程序一样构建复杂的多角色协议,并确信它们在任何环境下都能按预期执行。 + +## 核心思想:协议即状态图 + +在 Troupe 中,一个协议由一组**状态**组成,每个状态是一个 tagged union(标签联合体),它的每个字段代表一种可能的消息,而消息的“下一个状态”通过类型参数显式指定。例如: + +```zig +const Ping = union(enum) { + ping: Data(u32, Pong), + // ... +}; +``` + +`Data(Payload, NextState)` 是一个简单的包装,它携带实际数据,并指明了当这个消息被发送或接收后,协议应该进入的下一状态 `NextState`。 + +每个状态还附带一个编译期元信息 `info`,描述该状态在协议中的角色关系: + +- `sender`:本状态中谁负责发送消息。 +- `receiver`:谁将接收这个消息(可多个)。 +- `internal_roles`:参与本协议的所有角色集合。 +- `extern_state`:本协议结束后可能进入的“外部状态”列表(通常是其他协议的入口或出口)。 + +这些信息不仅是文档,更会被库用于编译期验证和运行时调度。 + +## 状态的执行模型:角色决定行为 + +运行时,每个角色(如 `alice`、`bob`)独立运行 `Runner.runProtocol` 函数,它根据当前状态和自身角色决定如何行动: + +- **如果角色是发送者**:调用该状态的 `process` 函数生成消息,然后将此消息通过通道发送给 `receiver` 列表中的所有角色。之后,根据消息中携带的 `NextState` 转移到下一状态。 +- **如果角色是接收者**:从通道接收消息(来自发送者),调用对应的预处理函数 `preprocess_N`(N 表示该角色在 `receiver` 列表中的位置),然后转移到消息指定的下一状态。 +- **如果角色不参与本轮通信**:说明此状态与他无关,他会直接跳过本轮,但可能会收到来自其他角色的“通知”(见后文),从而同步到新状态。 + +这种设计使得协议的执行路径对每个角色而言都是**唯一且确定**的:每个状态明确规定了谁发、谁收、发什么、下一步去哪。 + +## 分支状态与全员通知 + +当状态的 union 包含多个字段(即有多个分支选择)时,意味着协议在此处面临一个决策点。例如在“两阶段提交”的协调者状态中,协调者可能根据参与者反馈选择“提交”或“中止”。此时,只有发送者(协调者)知道最终选择了哪个分支。 + +为了让所有内部角色(即 `internal_roles` 中的其他角色)都能获知这个选择,**必须将产生的消息发送给除发送者外的所有内部角色**。这正是为什么库强制要求:当一个状态的 union 字段数大于 1 时,接收者列表必须覆盖所有其他内部角色,即满足 `1 + receiver.len == internal_roles.len`。 + +如果缺少任何一个内部角色,它将无法得知新状态,从而导致整个系统状态不一致。这条规则从根本上杜绝了“部分角色不知情”的问题,是 Troupe 保证全局确定性的基石。 + +## 协议组合:嵌套的状态图 + +Troupe 最强大的特性是能够将多个协议无缝组合成一个更大的协议。组合方式非常简单:将一个协议的入口状态作为另一个协议消息的 `NextState` 类型参数传入。 + +例如,在 `rp2pc` 示例中: + +```zig +charlie_as_coordinator: Data(void, PingPong(.alice, .bob, + PingPong(.bob, .charlie, + PingPong(.charlie, .alice, + CAB(@This()).Begin + ).Ping + ).Ping +).Ping), +``` + +这里 `PingPong` 是一个生成 ping-pong 协议状态的函数,它接受角色参数和下一个状态类型,返回包含 `Ping`、`Pong` 等状态的结构体。通过嵌套调用,我们可以让 ping-pong 结束后自动进入两阶段提交的 `Begin` 状态,形成一个复合协议。 + +在编译期,`reachableStates` 函数会递归展开所有嵌套的状态,构建出完整的全局状态图,并为每个状态生成唯一的整数 ID。同时,它会进行全面的验证: + +- 每个状态的发送者、接收者是否属于内部角色? +- 接收者中是否包含发送者?(不允许) +- 接收者是否有重复? +- 分支状态下未被通知的角色数量是否正确? +- 所有状态的上下文类型是否一致(按角色字段比对)? + +这些检查确保了组合后的协议依然是一个合法的确定性状态机。 + +## 跨协议同步:外部角色通知机制 + +当协议执行到某个被标记为“外部状态”(即出现在 `extern_state` 列表中的状态)时,意味着当前协议结束,即将进入另一个协议。为了保证所有角色(包括那些不参与当前协议的角色)都知道这一变化,Troupe 规定由 `internal_roles[0]`(即内部角色的第一个)向所有**外部角色**(不在 `internal_roles` 中的角色)发送一个特殊的 `Notify` 消息,其中包含新状态的 ID。 + +外部角色在下一轮循环中会首先接收这个通知,然后直接跳转到对应状态,从而与内部角色保持同步。这种“推式”同步避免了角色之间盲目轮询或猜测,确保了整个系统状态的一致迁移。 + +## 上下文聚合:数据共享的桥梁 + +不同协议可能需要访问同一角色的数据(例如计数器、随机数种子)。Troupe 通过一个**聚合的上下文结构体**来解决这个问题:开发者定义一个顶层 `Context`,其中每个角色对应一个字段,该字段包含了该角色可能需要的所有数据(包括各个协议子上下文的字段)。 + +例如: + +```zig +const Context = struct { + alice: AliceContext, + bob: BobContext, + charlie: CharlieContext, + selector: SelectorContext, +}; +``` + +在状态处理函数(`process` / `preprocess`)中,通过 `info.Ctx(role)` 获取当前角色对应的上下文类型,并在运行时传入指向该角色字段的指针。这样,不同协议可以通过同一角色的上下文共享数据,同时保持角色间的隔离。 + +## 编译期图遍历:安全性的最后防线 + +Troupe 在编译期通过 `reachableStates` 深度优先遍历所有可达状态,生成完整的状态列表和状态 ID 枚举。这个过程不仅用于构建运行时调度表,更重要的是它执行了大量的**一致性检查**: + +- 验证所有状态的上下文类型是否匹配(确保聚合上下文中的每个角色字段类型一致)。 +- 验证分支状态下接收者数量规则。 +- 验证发送者和接收者都在内部角色中。 +- 验证没有角色既是发送者又是接收者。 +- 验证外部状态列表中不包含内部状态(避免循环依赖)。 + +任何违反规则的情况都会导致编译错误,并给出清晰的提示。这意味着一旦程序编译通过,协议的组合就是合法的,运行时绝不会出现角色错配或状态丢失。 + +## 总结 + +Troupe 的设计体现了一种深刻的哲学:**将分布式协议的复杂性通过类型系统转化为可验证的确定性模型**。它把不确定性推给通信层,而让协议核心像剧本一样精确无误。开发者只需专注于定义状态、转移和角色行为,剩下的(调度、同步、验证)都由框架自动完成。 + +无论你是在实现一个简单的 ping-pong,还是一个包含多角色、多阶段的两阶段提交,甚至是这些协议的动态组合,Troupe 都能让你以**类型安全、可组合、编译期验证**的方式轻松构建,并最终运行出可靠、高效的分布式系统。 + +> **剧团(Troupe)的比喻**:每个角色是演员,协议是剧本,状态是场景,消息是台词。演员严格按照剧本演出,即使现场有意外(通信延迟),幕后工作人员(通道层)也会保证台词准确传递。观众看到的总是一场确定而精彩的表演。