Files
Agent-System/internal/docgov/engine.go
T
weichen a90cce73b9 fix(docgov): 保护共享内存 map,避免并发读写把进程打崩
per-doc 锁只串行同一文档,不同 docID 仍裸读写 memDocs 等 map。GetDocument 与另一文档写入撞上时运行时 fatal,代理把全部 API 变成 500。
2026-09-28 08:52:44 +00:00

114 lines
3.6 KiB
Go

package docgov
import (
"context"
"sync"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// GovernanceEngine 受控文档编制/审核/批准与认领治理引擎
type GovernanceEngine struct {
pool *pgxpool.Pool
sm *StateMachine
mu sync.RWMutex
// memMu 只保护下面四张共享 map 的结构。per-doc 锁串行同一文档的状态机,
// 不同 docID 仍会并发读写这些 map;T20 拆锁后漏了这层,会导致进程崩溃。
// 锁序:docLocks → e.mu → memMu。持有 memMu 时不得再获取 docLocks 或 e.mu。
memMu sync.RWMutex
memDocs map[string]*DocumentRecord
memComms map[string][]*DocumentComment
memProps map[string]*ChangeProposal
memAudits map[string][]*AuditLog
onApprovedHook func(ctx context.Context, doc *DocumentRecord)
onApprovedHooks []func(ctx context.Context, doc *DocumentRecord)
onPendingDigest PendingSupplementNotify
onLifecycleNotify LifecycleNotify
OnFinalArtifact func(ctx context.Context, doc *DocumentRecord, markdown []byte, snapshotID int64)
OnFinalArtifactTX func(ctx context.Context, tx pgx.Tx, doc *DocumentRecord, markdown []byte, snapshotID int64) error
// docLocks 按 doc_id 分锁(T20 B3);meta 全局锁只保护分锁表本身。
docLocks *docLockIndex
}
// LifecycleNotify is invoked after a successful governance state transition (outside engine lock).
type LifecycleNotify func(ctx context.Context, action string, doc *DocumentRecord, actorID int64, actorName, detail string)
// SetLifecycleNotify registers the post-transition notification hook.
func (e *GovernanceEngine) SetLifecycleNotify(hook LifecycleNotify) {
if e == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.onLifecycleNotify = hook
}
// SetPendingDigestNotify registers a callback when pending supplements are digested on return to drafting.
func (e *GovernanceEngine) SetPendingDigestNotify(hook PendingSupplementNotify) {
if e == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.onPendingDigest = hook
}
// SetOnApprovedHook 注册文档终审批准时的回调钩子
func (e *GovernanceEngine) SetOnApprovedHook(hook func(ctx context.Context, doc *DocumentRecord)) {
e.mu.Lock()
defer e.mu.Unlock()
e.onApprovedHook = hook
}
// AddOnApprovedHook 追加文档终审批准回调(与 SetOnApprovedHook 并存)。
func (e *GovernanceEngine) AddOnApprovedHook(hook func(ctx context.Context, doc *DocumentRecord)) {
if hook == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.onApprovedHooks = append(e.onApprovedHooks, hook)
}
func (e *GovernanceEngine) runApprovedHooks(ctx context.Context, doc *DocumentRecord) {
var primary func(context.Context, *DocumentRecord)
var extra []func(context.Context, *DocumentRecord)
e.mu.Lock()
primary = e.onApprovedHook
extra = append([]func(context.Context, *DocumentRecord){}, e.onApprovedHooks...)
e.mu.Unlock()
if primary != nil {
primary(ctx, doc)
}
for _, hook := range extra {
if hook != nil {
hook(ctx, doc)
}
}
}
func (e *GovernanceEngine) UsingPG() bool {
return e != nil && e.pool != nil
}
func (e *GovernanceEngine) RunApprovedHooksAfterCommit(ctx context.Context, doc *DocumentRecord) {
e.runApprovedHooks(ctx, doc)
}
// NewGovernanceEngine 初始化治理引擎
func NewGovernanceEngine(pool *pgxpool.Pool) *GovernanceEngine {
return &GovernanceEngine{
pool: pool,
sm: NewStateMachine(),
memDocs: make(map[string]*DocumentRecord),
memComms: make(map[string][]*DocumentComment),
memProps: make(map[string]*ChangeProposal),
memAudits: make(map[string][]*AuditLog),
docLocks: newDocLockIndex(),
}
}