Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions content/post/2026-02-19-troupe-1.smd
Original file line number Diff line number Diff line change
@@ -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; 否则可以忽略
Comment on lines +8 to +9

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comments on lines 8 and 9 are inconsistent in their formatting. Line 8 has an extra space between '请' and '设置', and both lines use a semicolon (;) as a separator in the Chinese text. It is recommended to unify the formatting, remove the extra space, and use Chinese punctuation (such as a comma) for better readability.

  .math = false, // 如果你要用到数学公式,请设置 math 为 true,否则可以忽略\n  .mermaid = false, // 如果你要用到 mermaid 图表,请设置 mermaid 为 true,否则可以忽略

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个是我从其他发布的文章复制过来的,不太懂这里的问题是什么。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这是我之前写得不规范,不用理会

},
---

# 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)的比喻**:每个角色是演员,协议是剧本,状态是场景,消息是台词。演员严格按照剧本演出,即使现场有意外(通信延迟),幕后工作人员(通道层)也会保证台词准确传递。观众看到的总是一场确定而精彩的表演。