在业务代码里驱动流程
启动它、喂它、停掉它,都发生在你平时写的代码里,比如一个 HTTP handler、一个支付回调、一个命令行工具。这些操作都走 Redis,调用方不必是跑流程的那个进程。
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 stepSendSignal
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,调度器没启动也照样能用。