Files
ai-agent/workflow/service/flow/exec_hub.go
T
19904408334 d699f7ce14 feat(workflow): 增加工作流计费与执行生命周期管理
- 新增计费模块:执行开始建单、终态结算/取消/失败处理,支持按条/按秒/按token计费
- 新增执行生命周期跟踪:优雅关停时取消运行中执行并等待落库
- 新增异步任务等待/通知机制(Wait/Notify)
- 重构执行记录落库与进度上报,统一失败分类与重试语义
- 重命名文件:async_task.go→async.go、flow_checkpoint_store.go→exec_checkpoint.go、flow_graph_util.go→exec_record.go
- 更新 .gitignore 与数据库密码配置
2026-09-03 13:22:22 +08:00

242 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package flow
import (
"context"
"fmt"
"sync"
"sync/atomic"
wsCommon "gitea.redpowerfuture.com/red-future/common/websocket"
)
// ====================== 执行事件中枢(attach to running execution ======================
//
// execHub 是单次工作流执行的事件中枢:把节点进度(node_start/node_complete)与终态
// flow_complete/error)广播到所有订阅的 WS 连接,并暴露执行取消入口(CancelByUser)。
//
// 背景:恢复例程捞起的执行没有 WS 连接、进度被丢弃;用户执行中再点"执行"时,现有代码只发
// round_start "正在执行中" 就返回,收不到进度也无法取消。execHub 让任意时刻建立的连接都能
// 订阅到同一 session+flow 运行中执行的后续进度,并通过 workflow_cancel 停止它。
//
// 生命周期:执行方(handleExecute 的新执行/断点续跑、recoverExecution 的恢复)创建并接管
// SetCancel + MarkOwnedhub,终态落库后 Publish 终态消息并 Close(退订全部连接、注销)。
// 候选 hub 由 handleExecute 提前注册(registerHubIfAbsent),同键已有运行中执行则复用其 hub。
type execHub struct {
sessionId string
flowId int64
execId int64 // 已知后设置,仅用于日志
mu sync.Mutex
subs map[*wsCommon.WsConnection]struct{}
cancel context.CancelFunc // 取消执行(用户执行=execCancel / 恢复=topCancel
owned bool // 已被执行方接管(MarkOwned);未接管视为候选/占位
closed bool
closeOnce sync.Once
done chan struct{}
userCancelled atomic.Bool
}
func newExecHub(sessionId string, flowId int64) *execHub {
return &execHub{
sessionId: sessionId,
flowId: flowId,
subs: make(map[*wsCommon.WsConnection]struct{}),
done: make(chan struct{}),
}
}
// ====================== 注册表(按 sessionId+flowId ======================
var (
hubRegMu sync.Mutex
hubReg = make(map[string]*execHub)
)
func hubKey(sessionId string, flowId int64) string {
return fmt.Sprintf("%s\x00%d", sessionId, flowId)
}
// registerHubIfAbsent 注册 hub;同键已有则返回现有 hub(候选丢弃,返回 created=false)。
// 保证同一进程内同 session+flow 同时只有一个 hub 对象,后续连接统一订阅到它。
func registerHubIfAbsent(sessionId string, flowId int64, hub *execHub) (existing *execHub, created bool) {
key := hubKey(sessionId, flowId)
hubRegMu.Lock()
defer hubRegMu.Unlock()
if h, ok := hubReg[key]; ok {
return h, false
}
hubReg[key] = hub
return hub, true
}
// ====================== 订阅 / 发布 ======================
func (h *execHub) Subscribe(conn *wsCommon.WsConnection) {
h.mu.Lock()
defer h.mu.Unlock()
h.subs[conn] = struct{}{}
}
func (h *execHub) Unsubscribe(conn *wsCommon.WsConnection) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.subs, conn)
}
// Publish 广播消息到所有订阅连接(跳过已关闭连接;WriteJSON 自带写锁,并发安全)
func (h *execHub) Publish(msg *wsCommon.WsPushMsg) {
h.mu.Lock()
conns := make([]*wsCommon.WsConnection, 0, len(h.subs))
for c := range h.subs {
conns = append(conns, c)
}
h.mu.Unlock()
for _, c := range conns {
if c.IsClosed() {
h.Unsubscribe(c)
continue
}
_ = writeJSON(c, msg)
}
}
// ReportStart / ReportComplete 实现 ProgressReporter:节点进度广播
func (h *execHub) ReportStart(nodeId, nodeName string, nodeIndex, nodeCount int) {
h.Publish(&wsCommon.WsPushMsg{
Type: "node_start",
Message: fmt.Sprintf("开始执行(%d/%d): %s ", nodeIndex, nodeCount, nodeName),
Data: map[string]interface{}{
"nodeId": nodeId, "nodeName": nodeName,
"nodeIndex": nodeIndex, "nodeCount": nodeCount,
},
})
}
func (h *execHub) ReportComplete(nodeId, nodeName string, nodeIndex, nodeCount int) {
h.Publish(&wsCommon.WsPushMsg{
Type: "node_complete",
Message: fmt.Sprintf("执行完成(%d/%d): %s ", nodeIndex, nodeCount, nodeName),
Data: map[string]interface{}{
"nodeId": nodeId, "nodeName": nodeName,
"nodeIndex": nodeIndex, "nodeCount": nodeCount,
},
})
}
// ====================== 接管 / 取消 ======================
// SetCancel 预留取消函数(候选 hub 在尚未确定执行方时设置,供用户提前取消)
func (h *execHub) SetCancel(cancel context.CancelFunc) {
h.mu.Lock()
h.cancel = cancel
h.mu.Unlock()
}
// MarkOwned 标记执行方已接管本 hub(真正开始 BuildExecution 前调用)
func (h *execHub) MarkOwned() {
h.mu.Lock()
h.owned = true
h.mu.Unlock()
}
// TryOwn 原子接管:仅当未被持有才置 owned 并设 cancel,返回是否抢到所有权。
// 并发恢复(扫描/用户附着)用它在锁/DB 前置位,避免"检查 Owned→MarkOwned"竞态下
// 后到者覆盖先到者的 cancel(用户取消会取消错 ctx)。失败者只附着订阅、不重复拉起执行。
func (h *execHub) TryOwn(cancel context.CancelFunc) bool {
h.mu.Lock()
defer h.mu.Unlock()
if h.owned {
return false
}
h.owned = true
h.cancel = cancel
return true
}
func (h *execHub) Owned() bool {
h.mu.Lock()
defer h.mu.Unlock()
return h.owned
}
// CancelByUser 用户显式取消(workflow_cancel):置 userCancelled 标志并取消执行。
// 恢复例程据此把终态写成"用户已终止执行 / retryable=0"(永久取消,不再被扫描捞起)。
func (h *execHub) CancelByUser() {
h.userCancelled.Store(true)
h.mu.Lock()
c := h.cancel
h.mu.Unlock()
if c != nil {
c()
}
}
func (h *execHub) UserCancelled() bool {
return h.userCancelled.Load()
}
// Close 关闭 hub:注销注册、退订全部连接、清其 meta、close donesync.Once,可被
// 执行方与占用方重复调用)。仅当连接 meta 仍指向本 hub 时清空(指针比较防误清新订阅)。
func (h *execHub) Close() {
h.closeOnce.Do(func() {
key := hubKey(h.sessionId, h.flowId)
hubRegMu.Lock()
if hubReg[key] == h {
delete(hubReg, key)
}
hubRegMu.Unlock()
h.mu.Lock()
conns := make([]*wsCommon.WsConnection, 0, len(h.subs))
for c := range h.subs {
conns = append(conns, c)
}
h.closed = true
h.mu.Unlock()
for _, c := range conns {
if cur, ok := wsCommon.GetMetaT[*execHub](c, "execHub"); ok && cur == h {
c.SetMeta("execHub", nil)
c.SetMeta("execCancel", nil)
}
}
close(h.done)
})
}
// Done 返回 hub 关闭通知 channel(供订阅 watcher 等退出)
func (h *execHub) Done() <-chan struct{} {
return h.done
}
// getProgressHub 从 context 取 hubreporter 即 hub 本身)
func getProgressHub(ctx context.Context) *execHub {
if h, ok := GetProgressReporter(ctx).(*execHub); ok {
return h
}
return nil
}
// subscribeConnToHub 把连接挂到 hub 上并接入取消:
// conn meta execCancel = hub.CancelByUser → 现有 workflow_cancel 处理器(handleCancel)直接生效。
// 断开连接不触发取消(用户执行经 closeCtx→execCtx 既有链取消;恢复执行脱离连接,订阅者仅观察)。
// 注意:必须转成 context.CancelFunc 再存,否则 GetMetaT[context.CancelFunc] 的类型断言
// (动态类型须与命名类型完全一致)会因方法值类型为 func() 而失败,取消静默失效。
func subscribeConnToHub(conn *wsCommon.WsConnection, hub *execHub) {
hub.Subscribe(conn)
conn.SetMeta("execHub", hub)
conn.SetMeta("execCancel", context.CancelFunc(hub.CancelByUser))
}
// execAttach 恢复例程附着的用户连接(用户点击执行发现陈旧运行中记录时传入)。
// hub 为连接已订阅的候选 hub:恢复例程复用同一实例接管(registerHubIfAbsent 按 session+flow 去重,
// 全进程同一时刻至多一个 hub 实例),避免"占位被关→重建"竞态导致连接订阅/取消丢失。
type execAttach struct {
conn *wsCommon.WsConnection
sessionId string
flowId int64
hub *execHub // 可能为 nil(调用方无候选时恢复例程自行注册)
}