feat(engine): 增加运行熔断、并发短路取消及生命周期钩子

This commit is contained in:
unclesam committed 2026-06-11 20:31:00 +08:00
1 parent 693c107473
commit 3a15b53b84
7 files changed
+761 -70

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
package GopherGraph
import "context"
// Engine 是 CompiledGraph 的增强包装器,在完全兼容原有 API 的基础上,
// 提供以下四项生产级能力:
//
// 1. StateCloner — 自定义深拷贝逻辑,消除并发分支的数据竞争隐患
// 2. MaxSteps — 步数硬性熔断,防止图中的环路导致无限循环
// 3. PreNodeHook — 节点执行前的生命周期钩子(日志、链路追踪等)
// 4. PostNodeHook — 节点执行后的生命周期钩子(指标上报、流式推送等)
//
// 所有选项均为可选,通过链式调用注册。未注册的选项将退回到 CompiledGraph 的默认行为。
//
// 用法示例:
//
// engine := GopherGraph.NewEngine(compiledGraph).
// WithStateCloner(func(s MyState) MyState {
// clone := s
// clone.Messages = append([]string{}, s.Messages...)
// return clone
// }).
// WithMaxSteps(100).
// WithPreNodeHook(func(ctx context.Context, name string, s MyState) {
// log.Printf("[PRE] node=%s", name)
// }).
// WithPostNodeHook(func(ctx context.Context, name string, s MyState) {
// log.Printf("[POST] node=%s value=%+v", name, s)
// })
//
// thread, err := engine.Start(ctx, "start", initialState)
type Engine[S any] struct {
graph *CompiledGraph[S]
stateCloner func(S) S
maxSteps int
preNodeHook HookFn[S]
postNodeHook HookFn[S]
}
// NewEngine 用一个已编译的图创建 Engine 实例。
func NewEngine[S any](graph *CompiledGraph[S]) *Engine[S] {
return &Engine[S]{graph: graph}
}
// WithStateCloner 注册自定义深拷贝函数。
//
// 当图中存在并发分支(AddParallelEdges)时,引擎会在启动每个并发 goroutine 之前
// 调用此函数对共享状态进行深拷贝,从根本上消除引用类型(切片、map、指针)的数据竞争。
//
// 若不注册,引擎退化为 Go 默认的值拷贝语义(浅拷贝)。
// 对于状态中含有引用类型的场景,强烈建议注册此函数。
func (e *Engine[S]) WithStateCloner(fn func(S) S) *Engine[S] {
e.stateCloner = fn
return e
}
// WithMaxSteps 设置引擎单次运行(Start/Resume)允许的最大节点执行步数。
//
// 这是防御图中存在意外环路(infinite loop)的最后一道硬性熔断。
// 超过上限后,引擎将返回明确的错误,而不是永久阻塞。
//
// 建议根据工作流的预期最大深度合理设置(如 100~1000)。
// 传入 0 或负数表示不限制(与不调用此方法等效)。
func (e *Engine[S]) WithMaxSteps(n int) *Engine[S] {
e.maxSteps = n
return e
}
// WithPreNodeHook 注册在每个节点执行【之前】触发的钩子函数。
//
// 典型用途:
// - 打印结构化日志(node 名称、输入状态)
// - 记录 OpenTelemetry Span 的开始时间
// - 向前端推送"正在执行 xxx 节点"的流式事件
//
// 钩子函数接收到的 state 是节点执行【前】的状态快照,请勿修改(只读语义)。
// 钩子中的 panic 会向上传播,建议在内部做好 recover。
func (e *Engine[S]) WithPreNodeHook(fn HookFn[S]) *Engine[S] {
e.preNodeHook = fn
return e
}
// WithPostNodeHook 注册在每个节点执行【之后】触发的钩子函数。
//
// 典型用途:
// - 打印节点的输出状态(用于 Debug)
// - 上报节点耗时指标(Prometheus/DataDog)
// - 向前端推送节点执行完毕的流式事件
//
// 钩子函数接收到的 state 是节点执行【后】更新过的状态,同为只读语义。
func (e *Engine[S]) WithPostNodeHook(fn HookFn[S]) *Engine[S] {
e.postNodeHook = fn
return e
}
// opts 将 Engine 的配置打包成 runOptions,传入底层 run() 调度器。
func (e *Engine[S]) opts() runOptions[S] {
return runOptions[S]{
maxSteps: e.maxSteps,
stateCloner: e.stateCloner,
preNodeHook: e.preNodeHook,
postNodeHook: e.postNodeHook,
}
}
// Start 从指定节点启动工作流,行为与 CompiledGraph.Start 完全一致,
// 但会附加所有已注册的 Engine 增强选项(Hooks、MaxSteps、StateCloner)。
func (e *Engine[S]) Start(ctx context.Context, startNode string, initialState S) (*Thread[S], error) {
thread := &Thread[S]{
State: initialState,
NextNode: startNode,
}
return e.graph.run(ctx, thread, e.opts())
}
// Resume 恢复执行一个被暂停的线程,行为与 CompiledGraph.Resume 完全一致,
// 但会附加所有已注册的 Engine 增强选项(Hooks、MaxSteps、StateCloner)。
func (e *Engine[S]) Resume(ctx context.Context, thread *Thread[S], modifiedState S) (*Thread[S], error) {
if !thread.IsPaused {
return nil, errNotPaused
}
if thread.IsFinished {
return nil, errAlreadyFinished
}
thread.State = modifiedState
thread.IsPaused = false
return e.graph.run(ctx, thread, e.opts())
}
// 复用与 CompiledGraph.Resume 中相同的哨兵错误,避免魔法字符串重复
var (
errNotPaused = errStr("cannot resume: thread is not paused")
errAlreadyFinished = errStr("cannot resume: thread is already finished")
)
type errStr string
func (e errStr) Error() string { return string(e) }