per-doc 锁只串行同一文档,不同 docID 仍裸读写 memDocs 等 map。GetDocument 与另一文档写入撞上时运行时 fatal,代理把全部 API 变成 500。
211 lines
5.4 KiB
Go
211 lines
5.4 KiB
Go
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)
|
||
}
|