Files
GopherGraph/engine_options.go
T
unclesam 5782504351 fix(engine): resolve parallel execution races and improve max steps semantics
- Fix data race in runParallelBranches where first error could be masked
- Improve MaxSteps to allow exactly N steps
- Consolidate Resume validation logic and fix silent overwrite in Graph AddNode/AddEdge
- Add sync.RWMutex to Engine for thread-safe configuration
- Fix P2 issues: Add immediate check for router output, add mutex to hooks example, add context timeout to translation example, and update docs for Checkpointer.Delete
2026-07-04 20:46:36 +08:00

168 lines
6.0 KiB
Go
Raw Permalink 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 GopherGraph
import (
"context"
"sync"
)
// Engine 是 CompiledGraph 的增强包装器,在完全兼容原有 API 的基础上,
// 提供以下四项生产级能力:
//
// 1. StateCloner — 自定义深拷贝逻辑,消除并发分支的数据竞争隐患
// 2. MaxSteps — 步数硬性熔断,防止图中的环路导致无限循环
// 3. PreNodeHook — 节点执行前的生命周期钩子(日志、链路追踪等)
// 4. PostNodeHook — 节点执行后的生命周期钩子(指标上报、流式推送等)
//
// 所有选项均为可选,通过链式调用注册。未注册的选项将退回到 CompiledGraph 的默认行为。
//
// # 线程安全说明
//
// Engine 使用 sync.RWMutex 保护配置字段,WithXxx 配置方法和 opts() 读取方法均加锁。
// 因此,允许在 Start/Resume 运行期间并发调用 WithXxx 修改配置(但不推荐这样做)。
// 常见的安全模式:在服务启动时一次性完成链式配置,之后多个 Goroutine 并发调用 Start/Resume。
//
// 用法示例:
//
// 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 {
mu sync.RWMutex
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.mu.Lock()
defer e.mu.Unlock()
e.stateCloner = fn
return e
}
// WithMaxSteps 设置引擎单次运行(Start/Resume)允许的最大节点执行步数。
//
// 这是防御图中存在意外环路(infinite loop)的最后一道硬性熔断。
// 超过上限后,引擎将返回明确的错误,而不是永久阻塞。
//
// 建议根据工作流的预期最大深度合理设置(如 100~1000)。
// 传入 0 或负数表示不限制(与不调用此方法等效)。
func (e *Engine[S]) WithMaxSteps(n int) *Engine[S] {
e.mu.Lock()
defer e.mu.Unlock()
e.maxSteps = n
return e
}
// WithPreNodeHook 注册在每个节点执行【之前】触发的钩子函数。
//
// 典型用途:
// - 打印结构化日志(node 名称、输入状态)
// - 记录 OpenTelemetry Span 的开始时间
// - 向前端推送"正在执行 xxx 节点"的流式事件
//
// 支持多次调用:每次调用将新钩子追加到执行链末尾,按注册顺序依次执行,不覆盖已有钩子。
// 钩子函数接收到的 state 是节点执行【前】的状态快照,请勿修改(只读语义)。
// 钩子中的 panic 会向上传播,建议在内部做好 recover。
func (e *Engine[S]) WithPreNodeHook(fn HookFn[S]) *Engine[S] {
e.mu.Lock()
defer e.mu.Unlock()
if e.preNodeHook == nil {
e.preNodeHook = fn
} else {
prev := e.preNodeHook
e.preNodeHook = func(ctx context.Context, name string, s S) {
prev(ctx, name, s)
fn(ctx, name, s)
}
}
return e
}
// WithPostNodeHook 注册在每个节点执行【之后】触发的钩子函数。
//
// 典型用途:
// - 打印节点的输出状态(用于 Debug)
// - 上报节点耗时指标(Prometheus/DataDog)
// - 向前端推送节点执行完毕的流式事件
//
// 支持多次调用:每次调用将新钩子追加到执行链末尾,按注册顺序依次执行,不覆盖已有钩子。
// 钩子函数接收到的 state 是节点执行【后】更新过的状态,同为只读语义。
func (e *Engine[S]) WithPostNodeHook(fn HookFn[S]) *Engine[S] {
e.mu.Lock()
defer e.mu.Unlock()
if e.postNodeHook == nil {
e.postNodeHook = fn
} else {
prev := e.postNodeHook
e.postNodeHook = func(ctx context.Context, name string, s S) {
prev(ctx, name, s)
fn(ctx, name, s)
}
}
return e
}
// opts 将 Engine 的配置快照打包成 runOptions,传入底层 run() 调度器。
// 使用 RLock 保证读取配置时的并发安全。
func (e *Engine[S]) opts() runOptions[S] {
e.mu.RLock()
defer e.mu.RUnlock()
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)。
// 复用 validateResume 进行前提条件校验,与 CompiledGraph.Resume 保持一致。
func (e *Engine[S]) Resume(ctx context.Context, thread *Thread[S], modifiedState S) (*Thread[S], error) {
if err := validateResume(thread); err != nil {
return nil, err
}
thread.State = modifiedState
thread.IsPaused = false
return e.graph.run(ctx, thread, e.opts())
}