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

211 lines
5.4 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 docgov
import (
"context"
"sync"
)
// docLockIndex 管理按 doc_id 的细粒度互斥锁。
// 全局锁只保护「分锁表」本身(短临界区);文档间操作互不阻塞。
// 锁序约定:docLocks.meta → docID(如需多文档,按 docID 字典序)。
type docLockIndex struct {
meta sync.Mutex
locks map[string]*sync.Mutex
}
func newDocLockIndex() *docLockIndex {
return &docLockIndex{locks: map[string]*sync.Mutex{}}
}
// lock 返回 docID 对应的互斥锁指针;调用方必须在 meta 锁内无其他副作用。
func (d *docLockIndex) lock(docID string) *sync.Mutex {
d.meta.Lock()
defer d.meta.Unlock()
mu, ok := d.locks[docID]
if !ok {
mu = &sync.Mutex{}
d.locks[docID] = mu
}
return mu
}
// docLockGuard 绑定一个 docID 的已获取互斥锁,确保按序释放。
type docLockGuard struct {
mu *sync.Mutex
release sync.Once
}
func (g *docLockGuard) Release() {
if g != nil && g.mu != nil {
g.release.Do(func() { g.mu.Unlock() })
}
}
func (e *GovernanceEngine) acquireDocLock(docID string) docLockGuard {
mu := e.docLocks.lock(docID)
mu.Lock()
return docLockGuard{mu: mu}
}
// withDocLock 以 docID 粒度执行 fn;fn 内的 DB I/O 仍持该文档锁,
// 但不再阻塞其他文档(原实现为全局 RWMutex)。
func (e *GovernanceEngine) withDocLock(docID string, fn func() error) error {
g := e.acquireDocLock(docID)
defer g.Release()
return fn()
}
// lookupMemDoc 返回内存中的文档记录(需在 doc 锁内调用)。
func (e *GovernanceEngine) lookupMemDoc(docID string) (*DocumentRecord, bool) {
return e.memDocsGet(docID)
}
// ensureDocLockedWithDB 在 doc 锁内做内存查找 + DB 兜底加载。
// 对应原 ensureDocumentLocked,但锁粒度为单文档。map 访问走 memMu,不再重入 e.mu
// (SaveDocumentContent 等调用方可能已经持有 e.mu)。
func (e *GovernanceEngine) ensureDocLockedWithDB(ctx context.Context, docID string) (*DocumentRecord, error) {
if doc, ok := e.memDocsGet(docID); ok {
return doc, nil
}
if e.pool != nil {
doc, err := e.loadDocumentFromDBLocked(ctx, docID)
if err == nil && doc != nil {
return doc, nil
}
}
return nil, ErrDocumentNotFound
}
func (e *GovernanceEngine) memDocsGet(docID string) (*DocumentRecord, bool) {
e.memMu.RLock()
defer e.memMu.RUnlock()
doc, ok := e.memDocs[docID]
return doc, ok
}
func (e *GovernanceEngine) memDocsPut(docID string, doc *DocumentRecord) {
e.memMu.Lock()
defer e.memMu.Unlock()
if e.memDocs == nil {
e.memDocs = map[string]*DocumentRecord{}
}
e.memDocs[docID] = doc
}
func (e *GovernanceEngine) memDocsFind(match func(*DocumentRecord) bool) *DocumentRecord {
e.memMu.RLock()
defer e.memMu.RUnlock()
for _, doc := range e.memDocs {
if doc != nil && match(doc) {
return doc
}
}
return nil
}
func (e *GovernanceEngine) forEachMemDoc(fn func(*DocumentRecord)) {
e.memMu.RLock()
defer e.memMu.RUnlock()
for _, doc := range e.memDocs {
fn(doc)
}
}
func (e *GovernanceEngine) memAuditsEmpty(docID string) bool {
e.memMu.RLock()
defer e.memMu.RUnlock()
return len(e.memAudits[docID]) == 0
}
func (e *GovernanceEngine) memAuditsAppend(docID string, audit *AuditLog) {
e.memMu.Lock()
defer e.memMu.Unlock()
e.memAudits[docID] = append(e.memAudits[docID], audit)
}
func (e *GovernanceEngine) memAuditsCopy(docID string) []*AuditLog {
e.memMu.RLock()
defer e.memMu.RUnlock()
logs := e.memAudits[docID]
res := make([]*AuditLog, len(logs))
copy(res, logs)
return res
}
func (e *GovernanceEngine) memCommsEmpty(docID string) bool {
e.memMu.RLock()
defer e.memMu.RUnlock()
return len(e.memComms[docID]) == 0
}
func (e *GovernanceEngine) memCommsAppend(docID string, comment *DocumentComment, assignSyntheticID bool) {
e.memMu.Lock()
defer e.memMu.Unlock()
if assignSyntheticID {
var maxID int64
for _, list := range e.memComms {
for _, c := range list {
if c != nil && c.ID > maxID {
maxID = c.ID
}
}
}
comment.ID = maxID + 1
}
e.memComms[docID] = append(e.memComms[docID], comment)
}
func (e *GovernanceEngine) memCommsCopy(docID string) []*DocumentComment {
e.memMu.RLock()
defer e.memMu.RUnlock()
comms := e.memComms[docID]
res := make([]*DocumentComment, len(comms))
copy(res, comms)
return res
}
func (e *GovernanceEngine) memCommsFind(commentID int64) (*DocumentComment, string) {
e.memMu.RLock()
defer e.memMu.RUnlock()
for id, list := range e.memComms {
for _, c := range list {
if c != nil && c.ID == commentID {
return c, id
}
}
}
return nil, ""
}
func (e *GovernanceEngine) memPropsGet(proposalID string) (*ChangeProposal, bool) {
e.memMu.RLock()
defer e.memMu.RUnlock()
p, ok := e.memProps[proposalID]
return p, ok
}
func (e *GovernanceEngine) memPropsPut(proposalID string, proposal *ChangeProposal) {
e.memMu.Lock()
defer e.memMu.Unlock()
e.memProps[proposalID] = proposal
}
func (e *GovernanceEngine) memPropsFind(match func(*ChangeProposal) bool) *ChangeProposal {
e.memMu.RLock()
defer e.memMu.RUnlock()
for _, p := range e.memProps {
if p != nil && match(p) {
return p
}
}
return nil
}
// loadDocumentFromDB 在无全局锁状态下从 PG 装载文档记录到内存。
// 这是 T20 B3 的核心:DB RTT 不再进入全局临界区。
func (e *GovernanceEngine) loadDocumentFromDB(ctx context.Context, docID string) (*DocumentRecord, error) {
e.mu.Lock()
defer e.mu.Unlock()
return e.loadDocumentFromDBLocked(ctx, docID)
}