# 重放模型 · gotick 文档

> 整个设计就是这一句话。API 为什么长这样、约束为什么是那几条、出错时会怎么错，都是从它推出来的。

文档

- [01快速上手](/docs.md)
- [02重放模型](/docs/model.md)
- [03底层实现](/docs/internals.md)
- [04核心 API](/docs/primitives.md)
- [05外部控制](/docs/control.md)
- [06示例](/docs/examples.md)
- [07运维与排查](/docs/operations.md)
- [08已知局限](/docs/limits.md)

# 每一轮调度，你的函数都从头再跑一遍

整个设计就是这一句话。API 为什么长这样、约束为什么是那几条、出错时会怎么错，都是从它推出来的。

本页目录

- [一轮是这样的](#loop)
- [一轮怎么中断：panic](#panic)
- [一共要跑多少轮](#rounds)
- [唯一的规矩：Task 之外必须确定](#determinism)
- [key 就是步骤的身份](#keys)

## 一轮是这样的

1. 1


Trigger 投一个事件



Trigger 往 Redis 上的队列写一个事件，然后把 callId 交给你。此刻你的流程一行都还没跑。

2. 2


某个 worker 重放你的函数



某个 worker 把事件取走，从你函数的第一行开始调用。取到事件的不一定是触发它的那个进程。注意它不是从中间接着跑，是整个重跑一遍。

3. 3


做完的步骤是读出来的，不是跑出来的



每一步先查自己的状态。已经完成的 Task 立刻返回，你的闭包不会被调用。Memo 直接把上次存下的值递回来，不会重新算一遍。

4. 4


第一个没做完的步骤执行，然后这一轮就结束



gotick 记下新状态，中断这一轮，再安排下一个事件。下一个事件可能是马上，也可能是你要求的三十分钟以后。下一轮做同样的事，往前挪一步。


## 一轮怎么中断：panic

这里没有协程可以挂起，也没有代码生成。需要等的那一步会抛一个断点出来，也就是一个带着 gotick.Breakpoint 值的 panic，调度器在这一轮的最外层把它 recover 掉。靠这个手法，流程才能是一个普通闭包：不需要 await，也没有任何特殊语法。

由此带来一个坑：在流程函数里、Task 之外裸写一个 recover()，会把断点一起吞掉，流程从此不再往前走，而且不报错。如果那里确实要 recover，先判断拿到的值是不是 gotick.Breakpoint，是就重新 panic 出去。Task 里面可以随便 recover，那段代码跑在正常的调用栈上。

复制

```
tick.Flow("demo", func(ctx *gotick.Context) {    defer func() {        if r := recover(); r != nil {            if _, isBreak := r.(gotick.Breakpoint); isBreak {                panic(r) // let the breakpoint through, or the flow stops here            }            log.Printf("flow panicked: %v", r)        }    }()     gotick.Task(ctx, "step", doWork)})
```

## 一共要跑多少轮

每个要写状态的步骤都占一轮，成功的也算：成功必须先落盘，下一步才允许开始。函数正常返回再占一轮。所以四个顺序 task 的流程，函数体至少执行五次。只有一处例外：重试退避只剩不到一秒时，gotick 就地等完，省掉一轮什么都没推进的重放。

## 唯一的规矩：Task 之外必须确定

同一次调用的两遍执行必须做出同样的判断。否则这一遍走 A 分支、下一遍走 B 分支，同一次调用就做了两件互相矛盾的事。这种错是静默的：不报错，只是偶尔有个订单既发了货又关了单。

| 你想做的事 | 别这么写 | 这么写 |
| --- | --- | --- |
| 看时间 | if time.Now().Hour() > 12 | 先用 Memo 把时刻固化，再拿它判断 |
| 查库 | user, \_ := db.GetUser(id) | gotick.Memo(ctx, "user", ...)，或者放进 Task 里查 |
| 随机数、UUID | id := uuid.NewString() | 生成一次，用 Memo 记住 |
| 遍历一个列表 | items, \_ := db.ListItems() | gotick.Array(ctx, "items", ...) 会记住列表和它的顺序 |
| 任何有副作用的动作 | mailer.Send(to, body) | 包进 gotick.Task 里 |

目前没有任何东西强制这一点，既没有静态检查，也没有运行期拦截。这是 gotick 最锋利的一道边，现在只写在文档里，还没写进代码里。

## key 就是步骤的身份

每个原语的第二个参数都是 key。gotick 靠 key 在两遍执行之间认出同一个步骤，不看行号，也不看调用顺序。所以同一个流程里两个步骤不能重名，撞了 key 会直接 panic，不会让后一个去读前一个的状态。key 还必须稳定：有实例在飞的时候改名，那一步会被当成新步骤从头再做。循环里的 key 每轮都要不一样，ArrayWrap.Key(prefix) 和 SequenceWrap.TaskKey(prefix) 就是干这个用的。

[← 上一篇\
\
快速上手](/docs.md) [下一篇 →\
\
底层实现](/docs/internals.md)

本页目录

- [一轮是这样的](#loop)
- [一轮怎么中断：panic](#panic)
- [一共要跑多少轮](#rounds)
- [唯一的规矩：Task 之外必须确定](#determinism)
- [key 就是步骤的身份](#keys)

> 全站页面清单：[/llms.txt](/llms.txt)
