# 外部控制 · gotick 文档

> 启动它、喂它、停掉它，都发生在你平时写的代码里，比如一个 HTTP handler、一个支付回调、一个命令行工具。这些操作都走 Redis，调用方不必是跑流程的那个进程。

文档

- [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)

# 在业务代码里驱动流程

启动它、喂它、停掉它，都发生在你平时写的代码里，比如一个 HTTP handler、一个支付回调、一个命令行工具。这些操作都走 Redis，调用方不必是跑流程的那个进程。

本页目录

- [Trigger](#trigger)
- [WithKey](#key)
- [Supersede](#supersede)
- [SendSignal](#signal)
- [Cancel](#cancel)

## Trigger

`func (t *Server) Trigger(ctx context.Context, flowId string, data MetaData, opts ...TriggerOption) (string, error)`

启动一个流程，拿回 callId。MetaData 就是流程的入参，流程里用 ctx.MetaData(k) 读。WithDelay(d) 让它晚一点再开始。

复制

```
callId, err := tick.Trigger(ctx, "order/close", gotick.MetaData{    "order_id": order.Id,    "user_id":  order.UserId,}, gotick.WithDelay(5*time.Second))
```

## WithKey

`func WithKey(key string) TriggerOption`

用你本来就有的标识给这次调用命名，比如订单号、用户 ID，之后就用它寻址。这样就不必在表上多存一列 gotick 生成的 callId，那一列对业务没有任何意义。作用域是 (flow, key)，两个不同的 flow 用同一个订单号不会互相干扰。

复制

```
_, err := tick.Trigger(ctx, "order/close", meta, gotick.WithKey(order.Id)) // later, from anywhere, with no callId storederr = tick.CancelByKey(ctx, "order/close", order.Id, "user canceled")
```

## Supersede

同一个 (flow, key) 再触发一次，前一个还没结束的调用自动取消，只有最后一次继续跑。旧的那次落进 canceled 终态，并写明被谁顶替了，不是凭空消失，检查界面上还看得到。调用一进终态，绑定立刻解除，同一个 key 之后还能再跑。注意这是顶替，不是去重：没有「已经有一个在跑就别开新的」这种模式。

复制

```
// each edit supersedes the unfinished run from the edit before itfor _, edit := range edits {    tick.Trigger(ctx, "doc/render", meta, gotick.WithKey(edit.DocId))}// only the last one reaches its final step
```

## SendSignal

`func (t *Server) SendSignal(ctx context.Context, callId, key string, value any) (bool, error)func (t *Server) SendSignalByKey(ctx context.Context, flowId, key, signal string, value any) (bool, error)`

把流程等的那个东西递进去。返回的 bool 表示这个信号有没有被采纳。false 说明那个位置已经定死了：要么被更早的信号占了，要么被超时占了。把 false 当情报看，不是错误，你就是靠它知道自己输了这场赛跑。

复制

```
// in your payment webhook handleraccepted, err := tick.SendSignalByKey(ctx, "order/close", orderId, "paid", Payment{    TradeNo: notify.TradeNo,    Amount:  notify.Amount,})if err != nil {    return err}if !accepted {    // the timeout already won: the order is closed, so refund instead    return refund(ctx, notify.TradeNo)}
```

## Cancel

`func (t *Server) Cancel(ctx context.Context, callId, reason string) errorfunc (t *Server) CancelByKey(ctx context.Context, flowId, key, reason string) error`

不管调用停在哪一步，都能让它停下。背后做了三件事：每一轮开头无条件读一次取消标志；正在睡觉或等信号的流程立刻唤醒，不用等它自己的定时器；正在执行的 Task 拿到的 context 会被 cancel，好让你的代码提前返回，这一件是每 3 秒轮询一次的。取消一个不存在的调用返回 ErrRunNotFound，取消一个已经结束的返回 ErrRunNotCancelable，所以迟到的取消改不了已经完成的结果。

复制

```
err := tick.Cancel(ctx, callId, "user canceled") switch {case errors.Is(err, gotick.ErrRunNotFound):    // never existed, or already cleaned upcase errors.Is(err, gotick.ErrRunNotCancelable):    // already finished — nothing was rewritten}
```

Client 有个限制：它只有 Trigger(ctx, flowId, data, delay)，没有 options，所以用不了 WithKey，也没有 SendSignal 和 Cancel。不跑调度的进程要用这些，就用 NewServerFromConfig 建一个 Server，然后不调 StartServer。触发、发信号、取消都走 Redis，调度器没启动也照样能用。

[← 上一篇\
\
核心 API](/docs/primitives.md) [下一篇 →\
\
示例](/docs/examples.md)

本页目录

- [Trigger](#trigger)
- [WithKey](#key)
- [Supersede](#supersede)
- [SendSignal](#signal)
- [Cancel](#cancel)

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