Files
ai-agent/workflow/dao/session/exec_workflow_dao.go
T

253 lines
10 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 session
import (
flow "ai-agent/workflow/consts/flow"
"ai-agent/workflow/consts/public"
sessionDto "ai-agent/workflow/model/dto/session"
"ai-agent/workflow/model/entity"
"context"
"fmt"
"time"
"gitea.redpowerfuture.com/red-future/common/beans"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
"github.com/gogf/gf/v2/util/gconv"
)
var ExecWorkflowDao = &execWorkflowDao{}
type execWorkflowDao struct{}
func (d *execWorkflowDao) Insert(ctx context.Context, req *sessionDto.CreateWorkflowReq) (id int64, err error) {
var s = new(entity.ExecWorkflow)
if err = gconv.Struct(req, &s); err != nil {
return
}
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).Insert(s)
if err != nil {
return
}
return r.LastInsertId()
}
func (d *execWorkflowDao) Delete(ctx context.Context, req *sessionDto.DeleteExecWorkflowReq) (rows int64, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).Where(entity.ExecWorkflowCol.Id, req.Id).Delete()
if err != nil {
return
}
return r.RowsAffected()
}
func (d *execWorkflowDao) Update(ctx context.Context, req *sessionDto.UpdateWorkflowReq) (rows int64, err error) {
if req.Id <= 0 {
return
}
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).OmitEmpty().Data(&req).Where(entity.ExecWorkflowCol.Id, req.Id).Update()
if err != nil {
return
}
return r.RowsAffected()
}
// UpdateMap 按列更新执行记录(map 更新不走 OmitEmpty,可显式写 0 值)。
// 供 recordWorkflow 终态一次原子落库(status/error_message/error/retryable/retry_count 同语句),
// 避免"先 UpdateRetry 再 Update"两步写部分生效导致 retryable 与状态不一致
func (d *execWorkflowDao) UpdateMap(ctx context.Context, id int64, data map[string]any) error {
if id <= 0 || len(data) == 0 {
return nil
}
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
Data(data).
Update()
return err
}
// ClearError 清空执行记录的报错信息(重新执行成功后调用,OmitEmpty 的 Update 会跳过空串,需显式写空)
func (d *execWorkflowDao) ClearError(ctx context.Context, id int64) (rows int64, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
Data(map[string]any{
entity.ExecWorkflowCol.ErrorMessage: "",
entity.ExecWorkflowCol.Error: "",
}).
Update()
if err != nil {
return
}
return r.RowsAffected()
}
func (d *execWorkflowDao) GetById(ctx context.Context, id int64) (res *entity.ExecWorkflow, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
One()
if err != nil {
return
}
err = r.Struct(&res)
return
}
func (d *execWorkflowDao) List(ctx context.Context, creator string, page *beans.Page) (res []*entity.ExecWorkflow, total int, err error) {
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Creator, creator)
m.OrderDesc(entity.ExecWorkflowCol.CreatedAt)
if page != nil {
m.Page(int(page.PageNum), int(page.PageSize))
}
r, total, err := m.AllAndCount(false)
if err != nil {
return
}
err = r.Structs(&res)
return
}
// GetLatestBySessionAndFlow 查询会话+工作流下最近一次执行记录(按创建时间倒序,无记录返回 nil)
func (d *execWorkflowDao) GetLatestBySessionAndFlow(ctx context.Context, sessionId string, flowId int64) (res *entity.ExecWorkflow, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.SessionId, sessionId).
Where(entity.ExecWorkflowCol.FlowId, flowId).
OrderDesc(entity.ExecWorkflowCol.CreatedAt).
Limit(1).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
// ListBySession 查询会话下工作流执行记录(按创建时间倒序)
func (d *execWorkflowDao) ListBySession(ctx context.Context, sessionId string) (res []*entity.ExecWorkflow, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.SessionId, sessionId).
OrderDesc(entity.ExecWorkflowCol.CreatedAt).
All()
if err != nil {
return
}
err = r.Structs(&res)
return
}
// ResetRunning 置为运行中并刷新心跳(execute 复用失败记录 / reExecute 共用;
// 用 map 更新避免 OmitEmpty 省略 0 值,同时写 status、last_heartbeat、node_group_id)。
// 条件更新防双跑:仅当记录当前状态仍属于 prevStatuses 之一时才重置(DB 行锁原子判定),
// 其余场景(已被恢复例程/并发触发抢先重置)返回 reset=false,调用方应放弃执行并返回 errExecAlreadyRunning
// 由持有方收敛状态,避免同一 exec 被两条路径并发 BuildExecution。
func (d *execWorkflowDao) ResetRunning(ctx context.Context, id int64, nodeGroupId string, prevStatuses ...int8) (reset bool, err error) {
m := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id)
if len(prevStatuses) > 0 {
statuses := make([]interface{}, 0, len(prevStatuses))
for _, s := range prevStatuses {
statuses = append(statuses, s)
}
m = m.WhereIn(entity.ExecWorkflowCol.Status, statuses)
}
r, err := m.Data(map[string]any{
entity.ExecWorkflowCol.Status: gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli(),
entity.ExecWorkflowCol.NodeGroupId: nodeGroupId,
}).Update()
if err != nil {
return false, err
}
rows, err := r.RowsAffected()
if err != nil {
return false, err
}
return rows > 0, nil
}
// ResetRunningIfRecoverable 恢复例程专用:条件重置为运行中,仅当记录仍处于可恢复状态时
// status=3 可重试失败,或 status=1 心跳陈旧的僵尸运行中)才重置(DB 行锁原子判定防双跑)。
// staleBeforeMsstatus=1 时的心跳陈旧阈值(毫秒),与 ListRecoverable/isRecoverable 判定一致。
// 返回 reset=false 表示状态已被其它路径(用户 reExecute/execute 或其它节点恢复)抢先重置,
// 本恢复例程应放弃续跑、不落终态,状态由持有方收敛。
func (d *execWorkflowDao) ResetRunningIfRecoverable(ctx context.Context, id int64, nodeGroupId string, staleBeforeMs int64) (reset bool, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
Where(fmt.Sprintf("(%s = ? OR (%s = ? AND %s < ?))",
entity.ExecWorkflowCol.Status,
entity.ExecWorkflowCol.Status,
entity.ExecWorkflowCol.LastHeartbeat),
gconv.Int8(*flow.FlowExecutionStatusFailed.Code()),
gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
staleBeforeMs).
Data(map[string]any{
entity.ExecWorkflowCol.Status: gconv.Int8(*flow.FlowExecutionStatusRunning.Code()),
entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli(),
entity.ExecWorkflowCol.NodeGroupId: nodeGroupId,
}).
Update()
if err != nil {
return false, err
}
rows, err := r.RowsAffected()
if err != nil {
return false, err
}
return rows > 0, nil
}
// TouchHeartbeat 更新执行心跳(毫秒时间戳),供后台心跳 goroutine 每 30s 调用一次
func (d *execWorkflowDao) TouchHeartbeat(ctx context.Context, id int64) error {
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
Data(map[string]any{entity.ExecWorkflowCol.LastHeartbeat: time.Now().UnixMilli()}).
Update()
return err
}
// UpdateRetry 更新重试标记与已重试次数(map 更新,retryable=0 也需写入)
func (d *execWorkflowDao) UpdateRetry(ctx context.Context, id int64, retryable int, retryCount int) error {
_, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
Where(entity.ExecWorkflowCol.Id, id).
Data(map[string]any{
entity.ExecWorkflowCol.Retryable: retryable,
entity.ExecWorkflowCol.RetryCount: retryCount,
}).
Update()
return err
}
// GetByIdNoTenant 跨租户按 id 读取执行记录(不追加 tenant_id 过滤)。
// 供恢复例程抢锁后、尚未知晓租户时重读 exec 使用;用户路径请继续用租户隔离的 GetById。
func (d *execWorkflowDao) GetByIdNoTenant(ctx context.Context, id int64) (res *entity.ExecWorkflow, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
NoTenantId(ctx).
Where(entity.ExecWorkflowCol.Id, id).
One()
if err != nil {
return
}
err = r.Struct(&res)
return
}
// ListRecoverable 返回可恢复执行:僵尸运行中(status=1 且心跳陈旧)或可重试失败(status=3 且 retryable=1 且未耗尽)。
// 调用方是跨租户的恢复扫描(无 HTTP 用户),显式 NoTenantId 走系统级扫描,避免僵尸执行因租户过滤漏检。
// ctx 必须携带 OTel spanNoTenantId 依赖 traceID 作为 gcache 标记键),恢复扫描由 scanAndRecover 保证。
func (d *execWorkflowDao) ListRecoverable(ctx context.Context, now int64, staleBefore int64, maxRetry int) (res []*entity.ExecWorkflow, err error) {
r, err := gfdb.DB(ctx, public.DbNameBlackDeacon).Model(ctx, public.TableNameExecWorkflow).
NoTenantId(ctx).
Where(fmt.Sprintf("(%s = ? AND %s < ?) OR (%s = ? AND %s = 1 AND %s < ?)",
entity.ExecWorkflowCol.Status, entity.ExecWorkflowCol.LastHeartbeat,
entity.ExecWorkflowCol.Status, entity.ExecWorkflowCol.Retryable, entity.ExecWorkflowCol.RetryCount),
gconv.Int8(*flow.FlowExecutionStatusRunning.Code()), staleBefore,
gconv.Int8(*flow.FlowExecutionStatusFailed.Code()), maxRetry).
All()
if err != nil {
return
}
// 用 All+Structs 而非 ScanScan 生成的列清单会丢嵌入 SQLBaseDO 的 id 等基础列,恢复例程需要 id
err = r.Structs(&res)
return
}