70 lines
4.8 KiB
Markdown
70 lines
4.8 KiB
Markdown
---
|
||
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 痛点。
|