Files
Agent-System/.doc/ops/short-term/2026-09-25-t20-s3-lock-split.md
T

70 lines
4.8 KiB
Markdown
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.
---
title: "T20 S3 锁批:B3 docgov/docrevision 分锁"
task_id: t20-s3-lock-split-2026-09-25
status: in_progress
created: 2026-09-25
updated: 2026-09-25
owner: Cursor
task_type: hardening
policy_revision: "AGENTS.md @ d93c1ab3"
branch: feature/t20-s3-lock-split-2026-09-25
worktree: .worktrees/t20-s3-lock-split-2026-09-25
base_dev_sha: "d93c1ab3d685e988f4bfc3f09a84bfc1e25f3302"
write_scope:
- internal/docrevision
- internal/docgov
- .doc/ops/short-term/2026-09-25-t20-s3-lock-split.md
- .doc/test-reports/2026-09-25-t20-s3-lock-split-test-report.md
mutex: docgov/docrevision
catalog: R-t20-s3-lock-split(overlap 探测无硬/软重叠)
review_state: done
integration_state: pending
deployment: not_required(纯进程内并发改造,无部署面变化)
cleanup: pending
remote_sync: not_requested
---
# T20 S3 锁批:B3 docgov/docrevision 分锁
## 用户目标与授权
- 来源:`planned-tasks-closeout-plan-2026-09-25` P4 批次;设计 [T20 B3](../../system-design/2026-09-22-t20-hardening-design.md)(阶段 A)。
- 目标:单文档慢 DB 查询不再阻塞**全部**文档的修订/租约路径。
## 实现方式(阶段 A,行为不变)
### docgov(`internal/docgov`)
- 新增 `lock_split.go`:`docLockIndex`(`map[docID]*sync.Mutex` + meta 锁只保护分锁表)与幂等 `docLockGuard`。
- 迁移写路径为 `acquireDocLock(docID)`:`AcquireLease`/`ReleaseLease`/`HeartbeatLease`/`SaveDocumentContent`/`AssertLeaseFencing`/`CheckHumanCanEdit`/`AssertWritableForComment`/`AddComment`/`CreateOrGetDocument`/`LockDocumentDuty`/`MarkDocumentContentUpdated`/`SetDocumentWorkDir`/`RecordDocumentViewed`/`RecordDocumentEdited`/`CheckModificationPermission`/`SubmitForReview`/`ReviewApprove`/`ReviewReject`/`ApproverApprove`/`ApproverReject`/`ReopenForRevision`/`GetDocument`/`ListAuditLogs`/`ListComments`/`ProposeModification`。
- 提案决策类(`DecideModificationProposal`/`MarkProposalApplied`)以 `*proposal:<id>` 命名空间分锁。
- `ApproverApprove` 的 hook-out-of-lock 段改用 guard 释放/重取(替代原 `e.mu.Unlock()/Lock()`)。
- 全局 `e.mu` 保留:`loadDocumentFromDBLocked` 内部短临界区写 `memDocs`、hooks 注册、`materialize` 模板段与 `hasCanonicalSibling` 内存兜底——均为纯内存操作,无 DB RTT。
### docrevision(`internal/docrevision`)
- 新增 `lock_split.go`:同构 `docLockIndex` + `Service.lockDoc`。
- 全部公开入口改为 **doc 锁 → 全局 `s.mu`** 两段式(`GetRevisionState`/`ListVersions`/`GetVersionContent`/`ManualOverlay`/`GetCandidate`/`GetChangeReport`/`CommitHumanRevision`/`OpenTemplateVersion`/`EnsureBootstrap`/`CreateAgentCandidate`/`CommitAgentVersion`/`ImportFreeformLegacy`/`SetDocWorkDir`/`SetLifecycle`/`EnsureRoot`/`ReconcileDocument`/`CommitMigrationRepair`/`InspectProjection`/`HeadContent`/`projectOutputMarkdown`/`ContentProjectionPending`/`MarkContentProjected`)。
- hydrate 的 PG 查询仍在 doc 锁内(保证同文档 TOCTOU 语义),但**不再进入跨文档临界路径**——这正是 B3「单文档慢查询阻塞全部文档」的修复点。
- 锁序统一:`docLocks[docID] → s.mu`(与 docgov `docLocks[docID] → e.mu` 对称);`docLocks.meta` 仅在查表瞬间持有。
## 关键设计决策
1. **hydrate 保持在 doc 锁内**(而非完全锁外):`lockOpenVersionTX` 的 `FOR UPDATE` 行锁 + 乐观 head/etag 校验依赖「检查-写入」原子性;锁外 hydrate + 短临界区合并需要引入版本 CAS,超出阶段 A「行为不变」边界(设计文档亦将完整缓存化列为阶段 B)。
2. **`sync.Once` 幂等 guard**:防止 `ApproverApprove` 这类显式释放/重取模式下的双重 Unlock panic(首轮 race 测试捕获后修复)。
3. **提案命名空间锁**:提案不属于单一文档的互斥域(决策跨 doc + proposal 表),独立命名空间避免与 doc 锁混序。
## 验证
- `go test -race ./internal/...`:67 包全绿(含 docgov/docrevision/app 下游)。
- 新增 `lock_split_test.go` 三用例:
- `TestParallelHydrateDoesNotSerializeAcrossDocs`:docA 持锁期间 docB 可完成锁定(验收断言);
- `TestParallelSameDocStillSerialized`:同文档串行不放松(回归保护);
- `TestConcurrentDistinctDocsCommits`:4 文档×10 版并行提交链无交叉污染。
- 既有并发负例(`TestConcurrentHumanRevisions`、`document_commit_concurrency_pg_test` 所在包)随 67 包回归通过。
## 未做(明确边界)
- 阶段 B(PG 为 SSOT、内存缓存化、锁外 hydrate + 版本 CAS):设计文档标注「需单独容量评估」,不与本批绑定。
- `SaveDocumentContent` 的 `e.mu.Lock()` 中段保留为全局锁(原代码即两段式:AcquireLease(已分锁)→ 内容写段);该段无 DB hydrate,仅 persist 一条 UPDATE,非 B3 痛点。