mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 22:56:55 +00:00
sw-block/design: T3 mini-plan + audit + sketch docs (pre-close docs)
Predecessor docs for the T3 batch, retained here for audit trail. The closure report (`v3-phase-15-t3-closure-report.md`), contract bridge catalogue, and BUG-005/006 artifacts already landed in commits `4127e5136` + `6e196885e`; this commit fills the docs those closure artifacts reference back to. Landed: v3-phase-15-t3-port-plan-sketch.md T3 umbrella sketch (rev-2.1, three-signed) v3-phase-15-t3-port-audit.md T3.0 port audit + Addendum A (QA-signed) v3-phase-15-t3a-mini-plan.md T3a scope + sign-off (CLOSED 0e1595c) v3-phase-15-t3b-mini-plan.md T3b scope + sign-off (CLOSED 72d0d40) v3-phase-15-t3c-mini-plan.md T3c scope + sign-off (CLOSED 829c6a9) Total 1,346 lines of doc; no code impact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6e196885e4
commit
88dcd49d67
@@ -0,0 +1,627 @@
|
||||
# V3 Phase 15 T3 — Function-Level Port Audit (T3.0)
|
||||
|
||||
**Status**: **LOCKED + QA single-signed 2026-04-22 per §8C.2** (see §8 sign-off table + §10.6)
|
||||
**Date**: 2026-04-22
|
||||
**Owner**: sw
|
||||
**Purpose**: Pre-code hard blocker for T3. Classify every public function of 7 V2 durable-layer muscle files into PORT-AS-IS / PORT-REBIND / DEFER / NEVER, each with explicit rebind target or stop-rule reference.
|
||||
**Context**: T3-start rev-2.1 §3.2 authority-boundary fix + §11.5 #4 require that storage **observes, recovers, and reports — does not publish**. Any V2 field, function, or comment that implies storage mints/advances/publishes authority facts (Epoch / EV / assignment state) must be reclassified.
|
||||
**Companion to**:
|
||||
- [v3-phase-15-t2-nvme-port-audit.md](v3-phase-15-t2-nvme-port-audit.md) — T2 production audit that precipitated this function-level discipline
|
||||
- [feedback_porting_discipline.md](../../.claude/projects/C--work-seaweedfs/memory/feedback_porting_discipline.md) — BUG-001 lesson: incidental V2 code is usually load-bearing
|
||||
|
||||
---
|
||||
|
||||
## 1. Classification
|
||||
|
||||
| Code | Meaning | Action on port | Audit rationale required |
|
||||
|---|---|---|---|
|
||||
| **PORT-AS-IS** | Pure mechanism, ports verbatim | Copy; adapt module paths + V3 log/imports only | 1-line why |
|
||||
| **PORT-REBIND** | Mechanism + non-authority binding (backend, metrics, frontend.Identity) | Copy; replace binding call sites | 1-line why + rebind target |
|
||||
| **DEFER** | In scope for T3 but not this batch | Out of T3a–T3c; document the target batch | 1-line why + target batch |
|
||||
| **NEVER** | Authority-coupled, boundary violation, or obsolete | Do NOT port; stop-rule citation mandatory | §3.2 fix citation + what V3 does instead |
|
||||
|
||||
Decision discipline: if a function **reads** or **writes** state that is an authority fact (Epoch / EV / role / lease / assigned subject), it is NEVER unless it can be reclassified as "observes last-published authority fact for local state comparison" with explicit §3.2 reconciliation path. The latter is PORT-REBIND with a comment explaining the semantic.
|
||||
|
||||
---
|
||||
|
||||
## 2. File inventory (7 durable-layer muscle files)
|
||||
|
||||
| # | V2 file | LOC | Why in T3.0 | Risk flag |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `superblock.go` | 298 | On-disk header; contains Epoch / ExpandEpoch fields | **#3 epoch** |
|
||||
| 2 | `wal_entry.go` | 153 | WAL record format (wire-stable) | — |
|
||||
| 3 | `wal_writer.go` | 302 | WAL append/advance/scan path | — |
|
||||
| 4 | `wal_admission.go` | 178 | WAL-pressure watermark gate | **#2 pressure** |
|
||||
| 5 | `dirty_map.go` | 156 | In-memory pending-flush set | — |
|
||||
| 6 | `group_commit.go` | 222 | fsync batching | — |
|
||||
| 7 | `write_gate.go` | 28 | Pre-write fencing check | **#1 fence** |
|
||||
|
||||
Out of T3.0 scope (second-wave audit for T3b / T3c batches, NOT this doc):
|
||||
`flusher.go`, `recovery.go`, `smartwal*.go`, `snapshot.go`, `scrub.go`, `replica_*.go`, `shipper_group.go`, `rebuild*.go`, `wal_shipper.go`. Each covered by its own mini-port-plan audit.
|
||||
|
||||
Out of T3 entirely (authority-coupled surface, NEVER — not revisited):
|
||||
`epoch.go`, `lease.go`, `role.go`, `promotion.go`, `dist_group_commit.go`, `repl_proto.go` authority portions.
|
||||
|
||||
---
|
||||
|
||||
## 3. File-by-file classification
|
||||
|
||||
### 3.1 `superblock.go` — on-disk header
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type Superblock struct` (layout) | **PORT-AS-IS** | Byte-for-byte on-disk format; version-gated. |
|
||||
| `type superblockOnDisk struct` | **PORT-AS-IS** | Same. |
|
||||
| `SuperblockSize`, `MagicSWBK`, `CurrentVersion` constants | **PORT-AS-IS** | On-disk contract. |
|
||||
| `NewSuperblock(volumeSize, opts)` | **PORT-REBIND** | Constructor takes `CreateOptions`; rebind to V3 create-path. Callers DO NOT pass Epoch — must be zero at create (see field note below). |
|
||||
| `(sb *Superblock) WriteTo(w io.Writer)` | **PORT-AS-IS** | Serialization; no semantic change. |
|
||||
| `ReadSuperblock(r io.Reader)` | **PORT-AS-IS** | Deserialization. |
|
||||
| `(sb *Superblock) Validate()` | **PORT-AS-IS** | Invariant check; purely local (magic, version, sizes, LSN monotonicity). |
|
||||
| `Superblock.Epoch` **field** | **PORT-REBIND (semantic)** | Persists as `uint64` for on-disk compat, BUT semantic rebinds from "fencing epoch authority writes here" to "last-observed assignment epoch, snapshot at time of Assignment.Accept". Storage NEVER advances this value. On startup, if superblock.Epoch > currently-assigned epoch → report NotReady with diagnostic evidence (§3.2 #4). Code comment + godoc rewritten. |
|
||||
| `Superblock.ExpandEpoch` **field** | **PORT-AS-IS** | Local operation-ID for expand flow; not an authority epoch. Keep semantic; rename to `ExpandOpID` during port if time allows to avoid future confusion. |
|
||||
| `Superblock.DurabilityMode` / `StorageProfile` / `PreparedSize` | **PORT-AS-IS** | Local configuration/state. |
|
||||
| `Superblock.WAL*` fields (Offset, Size, Head, Tail, CheckpointLSN) | **PORT-AS-IS** | Pure WAL geometry/state. |
|
||||
| `ErrNotBlockVol`, `ErrUnsupportedVersion`, `ErrInvalidVolumeSize`, `ErrInvalidSuperblock` | **PORT-AS-IS** | Local errors. |
|
||||
|
||||
**Risk #3 resolution**: `Epoch` field ports because the on-disk format is stable and the recovery comparison needs some local mirror. It is NOT a fencing knob — storage has no writeGate in V3 (see §3.7). The godoc **must** be rewritten to match §3.2; failure to update the godoc is a stop-rule #4 failure (advertised != implemented).
|
||||
|
||||
### 3.2 `wal_entry.go` — WAL record format
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type WALEntry struct` | **PORT-AS-IS** | On-disk record format. |
|
||||
| `(e *WALEntry) Encode()` | **PORT-AS-IS** | Serialization. |
|
||||
| `DecodeWALEntry(buf)` | **PORT-AS-IS** | Deserialization. |
|
||||
| WAL entry type constants (`WALEntryWrite`, `WALEntryTrim`, etc.) | **PORT-AS-IS** | Wire-stable enum. |
|
||||
|
||||
No authority coupling. Fully PORT-AS-IS.
|
||||
|
||||
### 3.3 `wal_writer.go` — WAL append path
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type WALWriter struct` | **PORT-AS-IS** | Local WAL state. |
|
||||
| `NewWALWriter(fd, walOffset, walSize, head, tail)` | **PORT-REBIND** | Constructor; rebind fd source from V2 `*os.File` to V3 backend fd accessor if V3 wraps the file in an abstraction. If V3 keeps raw `*os.File`, pure PORT-AS-IS. |
|
||||
| `physicalPos`, `used`, `UsedFraction` | **PORT-AS-IS** | Pure math. |
|
||||
| `Append(entry *WALEntry)` | **PORT-AS-IS** | Append path; no authority coupling. |
|
||||
| `writePadding` | **PORT-AS-IS** | Internal. |
|
||||
| `AdvanceTail(newTail)` | **PORT-AS-IS** | Local state transition. |
|
||||
| `Reset()` | **PORT-AS-IS** | Local state. |
|
||||
| `Head`, `Tail`, `LogicalHead`, `LogicalTail` | **PORT-AS-IS** | Accessors. |
|
||||
| `ScanFrom(fd, walOffset, ...)` | **PORT-AS-IS** | Recovery scan; local-only state. |
|
||||
| `Sync()` | **PORT-AS-IS** | fsync. |
|
||||
|
||||
No authority coupling. Fully PORT-AS-IS / PORT-REBIND.
|
||||
|
||||
### 3.4 `wal_admission.go` — WAL pressure gate
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type WALAdmission struct` | **PORT-AS-IS** | Pure mechanism (semaphore + watermarks). |
|
||||
| `type WALAdmissionConfig struct` | **PORT-REBIND** | Callbacks rebind: `NotifyFn` → V3 flusher wake; `ClosedFn` → V3 volume-host close signal; `Metrics` → V3 metrics facility. None are authority callbacks. |
|
||||
| `NewWALAdmission(cfg)` | **PORT-REBIND** | Uses Config above. |
|
||||
| `PressureState()` | **PORT-AS-IS** | Pure observation. |
|
||||
| `SoftPressureWaitNs`, `HardPressureWaitNs`, `SoftMark`, `HardMark` | **PORT-AS-IS** | Accessors. |
|
||||
| `Acquire(timeout)` | **PORT-AS-IS** | Blocking admission; no authority surface. |
|
||||
| `recordAdmit`, `Release` | **PORT-AS-IS** | Local. |
|
||||
| `ErrWALFull`, `ErrVolumeClosed` | **PORT-AS-IS** | Local errors. |
|
||||
|
||||
**Risk #2 resolution**: WAL pressure is pure local mechanism. No function reads/writes Epoch / EV / role. `NotifyFn` wakes flusher (local); `ClosedFn` queries volume closure (local). **No reclassification needed** — the risk was "conflated with authority backpressure", but V2 already kept them separate. Ports cleanly.
|
||||
|
||||
Stop-rule guard: if during T3 port any caller wires `NotifyFn` or `ClosedFn` to an authority-ringing callback, that's a §3.2 violation. Port-time QA should grep wiring sites.
|
||||
|
||||
### 3.5 `dirty_map.go` — in-memory pending flush set
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type dirtyEntry`, `dirtyShard`, `DirtyMap`, `rangeEntry`, `SnapshotEntry` | **PORT-AS-IS** | In-memory sharded map. |
|
||||
| `NewDirtyMap(numShards)` | **PORT-AS-IS** | Constructor. |
|
||||
| `shard(lba)` | **PORT-AS-IS** | Internal shard lookup. |
|
||||
| `Put(lba, walOffset, lsn, length)` | **PORT-AS-IS** | Covered by feedback_porting_discipline.md Phase 08 lesson: V2's compare-and-delete pattern (LBA + expected LSN match) MUST port verbatim. |
|
||||
| `Get(lba)` | **PORT-AS-IS** | |
|
||||
| `Delete(lba)` | **PORT-AS-IS** | **CRITICAL** — V2 uses compare-and-delete (LBA + expected LSN). Simplifying to unconditional delete-by-LBA is the Phase 08 bug (see memory feedback entry). sw must copy the V2 algorithm verbatim. |
|
||||
| `Range(start, count, fn)` | **PORT-AS-IS** | Iteration. |
|
||||
| `Clear()` | **PORT-AS-IS** | Used during recovery / rebuild. |
|
||||
| `Len()` | **PORT-AS-IS** | Accessor. |
|
||||
| `Snapshot()` | **PORT-AS-IS** | For flusher batching; LSN-sorted per Phase 08 lesson. |
|
||||
|
||||
Stop-rule guard: Phase 08 lesson — V2's sort-by-LSN-before-batching MUST port. Any V3 "simplified" version that drops the sort will re-introduce the data-loss bug on recovery.
|
||||
|
||||
### 3.6 `group_commit.go` — fsync batching
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `type GroupCommitter struct` | **PORT-AS-IS** | Local batching mechanism. |
|
||||
| `type GroupCommitterConfig struct` | **PORT-REBIND** | Callbacks rebind: `SyncFunc` → V3 backend.Sync; `OnDegraded` → V3 local health event (consumed by T1 volume-host, ringed to master via heartbeat — observation path, NOT authority publish); `PostSyncCheck` → **DROP** (see row below); `Metrics` → V3 metrics. |
|
||||
| `NewGroupCommitter(cfg)` | **PORT-REBIND** | Uses Config above. |
|
||||
| `Run()` | **PORT-AS-IS** | Main loop; local mechanism. |
|
||||
| `Submit()` | **PORT-AS-IS** | Waiter registration. |
|
||||
| `Stop()`, `markStoppedAndDrain` | **PORT-AS-IS** | Shutdown. |
|
||||
| `SyncCount()` | **PORT-AS-IS** | Test accessor. |
|
||||
| `callSyncFunc()` | **PORT-AS-IS** | Wraps syncFunc with panic-guard. |
|
||||
| **PostSyncCheck semantic** | **NEVER (semantic)** | In V2 this hook fired `writeGate()` post-sync to detect fencing expiry during a batch. V3 has no writeGate; callers must NOT populate `PostSyncCheck` with any authority check. Keep the struct field for V2 port-fidelity (it's an optional callback; nil is valid), but the V3 constructor's default MUST leave it nil, and port-time QA greps callers to confirm none wire an authority check into it. |
|
||||
|
||||
Rebind targets summary:
|
||||
- `SyncFunc` → V3 backend / fd-provider
|
||||
- `OnDegraded` → V3 local health event stream (observed by volume host → heartbeat → master, per §3.2)
|
||||
- `PostSyncCheck` → nil; NEVER rebind to authority check
|
||||
- `Metrics` → V3 metrics
|
||||
|
||||
### 3.7 `write_gate.go` — fencing check
|
||||
|
||||
| Function / Type | Classification | Rebind / rationale |
|
||||
|---|---|---|
|
||||
| `(v *BlockVol) writeGate()` | **NEVER** | §3.2 fix + §11.5 #4: storage does not fence. V3 frontend already checked identity via `frontend.Backend`; by the time storage sees a Write, authority validity is the frontend's problem. No storage-side fencing. |
|
||||
| `ErrNotPrimary`, `ErrEpochStale`, `ErrLeaseExpired` | **NEVER** | Authority-surface errors. Equivalent errors in V3 live in the authority package and surface via `frontend.Backend.Write` returning `frontend.ErrStalePrimary` / `frontend.ErrBackendClosed`. Storage never emits them. |
|
||||
|
||||
**Risk #1 resolution**: `write_gate.go` does NOT port. The whole file is NEVER. This is the cleanest outcome — single-function file, unambiguous. §3.2 fix #4 makes this call explicitly: "If local state disagrees with master-published, report NotReady + evidence; do not act as authority." writeGate is the classic act-as-authority pattern.
|
||||
|
||||
If sw accidentally ports writeGate (copy-paste from V2), that's a §8C.3 trigger #4 escalation — sw stops, user signs a Discovery Bridge acknowledging the audit miss, then removes it.
|
||||
|
||||
---
|
||||
|
||||
## 4. Rebind target reference
|
||||
|
||||
All PORT-REBIND decisions in this audit reference one of:
|
||||
|
||||
| Target | Description | Example |
|
||||
|---|---|---|
|
||||
| `frontend.Backend` | V3 per-volume backend interface (already used by T2 iSCSI/NVMe) | wal_writer fd source |
|
||||
| `frontend.Identity` | Opaque authority-identity handle, passed through to storage but never inspected for fencing | — |
|
||||
| V3 metrics facility | (name TBD — likely `core/metrics` or similar) | GroupCommitter.Metrics |
|
||||
| V3 local health event stream | T1-built volume host (path not yet named in T3-start) | GroupCommitter.OnDegraded |
|
||||
| V3 volume-host close signal | T1 coordination of volume lifecycle | WALAdmission.ClosedFn |
|
||||
|
||||
Any rebind target not in this list surfaces during T3a–T3e port work; new targets require mini-port-plan §5.2 note.
|
||||
|
||||
---
|
||||
|
||||
## 5. Stop-rule application
|
||||
|
||||
Three stop-rules inform this audit (from T3-start §8 + BUG-001 lessons):
|
||||
|
||||
| # | Rule | Trigger in this audit |
|
||||
|---|---|---|
|
||||
| 1 | Port means port — do not simplify | dirty_map.go `Delete` + `Snapshot` — V2's sort + compare-delete MUST port verbatim (Phase 08 lesson) |
|
||||
| 2 | Mechanism vs policy — policy goes elsewhere | write_gate.go (policy = fencing) → NEVER |
|
||||
| 3 | Observed fact vs published fact | Superblock.Epoch → PORT-REBIND (observed), not PORT-AS-IS with V2 "fencing" semantic |
|
||||
| 4 | Advertised ≡ implemented | Superblock.Epoch godoc MUST be rewritten to match observation-only semantic |
|
||||
|
||||
---
|
||||
|
||||
## 6. Out-of-audit observations
|
||||
|
||||
Things I noticed during review that are NOT function-level classifications but deserve flagging for T3a mini port plan:
|
||||
|
||||
1. **`CreateOptions` struct** (referenced in superblock.go `NewSuperblock`) — not inspected in this audit. T3a plan should classify it separately if its fields cross into authority surface.
|
||||
2. **`EngineMetrics`** — referenced by `WALAdmission` and `GroupCommitter`. V3 needs a concrete metrics interface with matching shape; TBD in T3a design doc.
|
||||
3. **`ErrVolumeClosed`** — used by WALAdmission; needs a V3 home. Probably co-located with the new volume-host close signal.
|
||||
4. **Phase 4A residue**: the V2 `Superblock.Epoch` comment says "fencing epoch (0 = no fencing, Phase 3 compat)". That comment is Phase 4A-era. V3 port MUST rewrite the comment; any leftover Phase-4A language is a §3.2 leak.
|
||||
|
||||
---
|
||||
|
||||
## 7. Summary matrix
|
||||
|
||||
| File | PORT-AS-IS | PORT-REBIND | DEFER | NEVER |
|
||||
|---|:-:|:-:|:-:|:-:|
|
||||
| superblock.go | 8 | 2 | 0 | 0 |
|
||||
| wal_entry.go | 4 | 0 | 0 | 0 |
|
||||
| wal_writer.go | 11 | 1 | 0 | 0 |
|
||||
| wal_admission.go | 8 | 2 | 0 | 0 |
|
||||
| dirty_map.go | 12 | 0 | 0 | 0 |
|
||||
| group_commit.go | 7 | 2 | 0 | 1 (semantic) |
|
||||
| write_gate.go | 0 | 0 | 0 | 2 |
|
||||
| **totals** | **50** | **7** | **0** | **3** |
|
||||
|
||||
50 PORT-AS-IS → the bulk is faithful port, consistent with V2 being mature mechanism.
|
||||
7 PORT-REBIND → rebind targets all local (metrics / backend / volume-host); no authority publish.
|
||||
0 DEFER → these 7 files are T3a foundation; no deferrals.
|
||||
3 NEVER → all risk-flagged (write_gate 2 + GroupCommitter.PostSyncCheck semantic).
|
||||
|
||||
---
|
||||
|
||||
## 8. Sign-off
|
||||
|
||||
| Role | Name | Date | Status |
|
||||
|---|---|---|---|
|
||||
| sw | Claude (sw agent) | 2026-04-22 | ✅ initial draft (§1-§9) + §10.7 sanity confirmation (5/5 findings verified) + §13 Q2/Q3/Q4 LOCKED |
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ **SIGNED** per §8C.2 — §10.3 walk-through LOCKED (48 MATCHES + 2 MATCHES-BETTER + 0 VIOLATIONS), Addendum A (impl-identity + matrix) landed, Option 3 for Epoch gap deferred to T3b per §10.5 |
|
||||
|
||||
QA review checklist (per §8C.5 + T3-start):
|
||||
- [ ] Every public function appears exactly once
|
||||
- [ ] Every PORT-REBIND has an explicit rebind target
|
||||
- [ ] Every NEVER cites §3.2 or a stop-rule
|
||||
- [ ] Risk #1 (write_gate) classified NEVER
|
||||
- [ ] Risk #2 (WAL pressure) classified PORT-AS-IS / PORT-REBIND with no authority coupling
|
||||
- [ ] Risk #3 (Superblock.Epoch) classified PORT-REBIND with semantic note, not PORT-AS-IS
|
||||
- [ ] Phase 08 dirty_map lesson cited explicitly
|
||||
- [ ] Second-wave files (flusher / recovery / smartwal / etc.) called out as OUT of this audit
|
||||
- [ ] No function escalates §8C.3 trigger #4 after classification
|
||||
|
||||
---
|
||||
|
||||
## 9. Change log
|
||||
|
||||
| Date | Change | Author |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | Initial draft covering 7 T3.0 foundation files; 50 / 7 / 0 / 3 split | sw |
|
||||
| 2026-04-22 | §10–§14 appended: scope reframed per user direction "keep V3 `core/storage/`, find gaps"; sw's §1–§9 V2 function classification reinterpreted as **validation spec** (V3 existing port must match); integration-layer gap analysis + operational-gate API lock + 4-batch split added | QA + sw joint |
|
||||
|
||||
---
|
||||
|
||||
## 10. Scope reframe — sw's §1–§9 as validation spec, not re-port plan
|
||||
|
||||
After sw's audit was drafted, user direction LOCKED (see chat 2026-04-22 late afternoon):
|
||||
|
||||
> *"V3 设计了 interface 本来是想支持 regular/smartWAL,比 V2 好,所以保留 interface 和核心已经 test 过的 storage code,其他看看差什么。"*
|
||||
|
||||
V3 `core/storage/` commit `97a9695` + `9004c74` (2026-04-16) landed a pluggable `LogicalStorage` interface + 3 implementations (`BlockStore` + `WALStore` + `smartwal.Store`); tests green. This is **kept**. T3 does NOT re-port V2 files into V3 from scratch.
|
||||
|
||||
**Sw's §1–§9 reinterpretation** (same content, different use):
|
||||
|
||||
| Sw said | Reframed as |
|
||||
|---|---|
|
||||
| "PORT-AS-IS: copy verbatim from V2" | **V3 must exhibit the same behavior**; QA verifies by cross-reading V3 impl vs sw's V2 classification |
|
||||
| "PORT-REBIND: copy, rebind call sites" | **V3's rebind target must match sw's expectation**; if V3 already rebinds correctly, no action; if V3 rebinds to wrong target (e.g., authority surface), §8C.3 trigger #4 escalate |
|
||||
| "NEVER: do not port" | **V3 must NOT contain this behavior**; QA greps V3 `core/storage/` to confirm absence (e.g., `writeGate` must not exist) |
|
||||
| "DEFER" (none in sw's audit) | N/A for this audit |
|
||||
|
||||
This reframe turns sw's 60-row function table into a **validation checklist** for QA to walk over `core/storage/`. Each row becomes a "V3 is/isn't doing X" question.
|
||||
|
||||
### 10.1 V3 existing-port validation (QA to complete before T3a opens)
|
||||
|
||||
QA walks each row of §3.1–§3.7 against V3 `core/storage/` and marks:
|
||||
- ✅ **MATCHES** — V3 impl agrees with sw's classification
|
||||
- ⚠️ **DRIFT** — V3 deviates; specify how (may still be acceptable if deviation is defensible — e.g., V3's better interface)
|
||||
- ❌ **GAP** — V3 is missing a behavior sw's classification says must exist; T3a must add
|
||||
- 🚫 **VIOLATION** — V3 contains a NEVER behavior; §8C.3 escalate
|
||||
|
||||
Completion blocks T3a sign. QA estimates 1-2 hours for 60 rows.
|
||||
|
||||
### 10.2 Initial spot-checks (QA pre-audit, not comprehensive)
|
||||
|
||||
| Item | V3 `core/storage/` status | Verdict |
|
||||
|---|---|---|
|
||||
| `writeGate` function (sw: NEVER) | grep shows zero occurrences in `core/storage/` | ✅ no violation |
|
||||
| `Superblock.Epoch` field (sw: PORT-REBIND with observation-only semantic) | `superblock.go` has epoch field; advancement path not yet audited | ⚠️ needs QA §10.1 walk-through |
|
||||
| `DirtyMap.Delete` compare-and-delete (sw: CRITICAL — Phase 08 lesson) | `dirty_map.go` behavior not yet audited | ⚠️ needs QA §10.1 walk-through + test assertion |
|
||||
| `GroupCommitter.PostSyncCheck` field (sw: NEVER semantic; optional field kept but default nil) | V3 existing `group_commit.go` presence unknown; QA to check | ⚠️ needs QA §10.1 walk-through |
|
||||
| `WALAdmission.Acquire / PressureState` (sw: pure mechanism) | `wal_admission.go:90 Acquire(timeout)`, line 71 `PressureState()` — matches V2 shape | ✅ signature match (behavior match TBV) |
|
||||
|
||||
Full §10.1 walk-through below in §10.3.
|
||||
|
||||
### 10.3 §10.1 V3 existing-port walk-through (QA completed 2026-04-22)
|
||||
|
||||
Each row: sw's V2 classification → V3 `core/storage/` verdict.
|
||||
|
||||
Legend: ✅ MATCHES / ⚠️ DRIFT (acceptable) / ❌ GAP / 🚫 VIOLATION
|
||||
|
||||
#### 10.3.1 `superblock.go` (sw §3.1)
|
||||
|
||||
| Sw row | V3 equivalent | Verdict |
|
||||
|---|---|---|
|
||||
| `type Superblock struct` layout PORT-AS-IS | `type superblock` pkg-private, line 32 | ✅ MATCHES |
|
||||
| `type superblockOnDisk` PORT-AS-IS | embedded serialization | ✅ MATCHES |
|
||||
| `SuperblockSize/MagicSWBK/CurrentVersion` | lines 12-30 | ✅ MATCHES |
|
||||
| `NewSuperblock(volumeSize, opts)` PORT-REBIND | `newSuperblock(volumeSize, opts createOptions)` line 50; `createOptions` has BlockSize/ExtentSize/WALSize only — **no Epoch param** | ✅ MATCHES |
|
||||
| `WriteTo / ReadSuperblock / Validate` | `writeTo` (82) / `readSuperblock` (115) / `validate` (162) | ✅ MATCHES |
|
||||
| **`Superblock.Epoch` field PORT-REBIND** | **NOT PRESENT in V3** — zero grep matches | ❌ **GAP (non-blocking)** — see §10.5 |
|
||||
| `Superblock.ExpandEpoch` PORT-AS-IS | NOT PRESENT in V3 | ❌ GAP (T3 OUT; expand flow not in G4 canonical) |
|
||||
| DurabilityMode / StorageProfile / PreparedSize PORT-AS-IS | NOT PRESENT in V3 (V3 createOptions simpler) | ⚠️ DRIFT (acceptable — V3 subset sufficient for G4) |
|
||||
| WAL fields (Offset/Size/Head/Tail/CheckpointLSN) | `WALCheckpointLSN` present (line 250 write-back path) | ✅ MATCHES — G-int.6 row 2 checkpoint-LSN write-back CONFIRMED |
|
||||
| Error vars | lines 18-30 | ✅ MATCHES |
|
||||
|
||||
#### 10.3.2 `wal_entry.go` (sw §3.2)
|
||||
|
||||
All 4 rows MATCH. `walEntry` (line 49), `encode` (63), `decodeWALEntry` (114), entry type constants (line 11).
|
||||
|
||||
#### 10.3.3 `wal_writer.go` (sw §3.3)
|
||||
|
||||
11/12 MATCH. `walWriter` (26), `newWALWriter` fd-as-raw-`*os.File` (38), `physicalPos`/`used`/`usedFraction` (48/50/186), `append` (58), `writePadding` (101), `advanceTail` (134), `reset` (149), accessors (156/162/168/174), `fsync` (181; semantic = V2 `Sync`). `ScanFrom` → 1 row DEFER for sw spot-check T3a.
|
||||
|
||||
#### 10.3.4 `wal_admission.go` (sw §3.4)
|
||||
|
||||
9/10 MATCH. `walAdmission` (30), `walAdmissionConfig` with NotifyFn+ClosedFn (44), `newWALAdmission` (58), `PressureState` (71), accessors (82-85), `Acquire` (90), `Release` (156), `errVolumeClosed` (11). Doc comment at line 52-56 explicitly "Behavior mirrors V2's weed/storage/blockvol/wal_admission.go" — **G-int.6 row 1 WAL-pressure 1:1 CONFIRMED**. Metrics callback absent (⚠️ DRIFT acceptable — V3 has no metrics facility yet; G17/T8 territory).
|
||||
|
||||
#### 10.3.5 `dirty_map.go` (sw §3.5)
|
||||
|
||||
10/12 MATCH + 1 MATCHES-BETTER + 1 VERIFY. `dirtyMap` (23), `newDirtyMap` (30), `shard` (44), `put` (46), `get` (53), `clear` (134), `len` (123), `snapshot` (108). **MATCHES-BETTER**: V3 has **both** `delete(lba)` (64, unconditional) AND `compareAndDelete(lba, expectedLSN)` (85). Flusher uses `compareAndDelete` (flusher.go:171, 236) — **Phase 08 lesson correctly applied**. Unconditional `delete()` has **zero callers in storage package** — footgun unexercised. VERIFY: snapshot LSN-sort invariant needs sw spot-check in T3a test; `Range` not found line-by-line (DEFER spot-check).
|
||||
|
||||
#### 10.3.6 `group_commit.go` (sw §3.6)
|
||||
|
||||
7/9 MATCH + 1 MATCHES-BETTER + 1 DRIFT. `GroupCommitter` (20), `NewGroupCommitter` (43), `Run` (61), `Stop` (155), `markStoppedAndDrain` (122), `SyncCount` (164), `fsyncSafe` (113; = V2 `callSyncFunc`). **MATCHES-BETTER**: `GroupCommitterConfig` has NO `PostSyncCheck` field — **mechanically cannot wire authority check** (sw's NEVER semantic achieved by absence, stronger than sw's spec). ⚠️ DRIFT: V3 `GroupCommitterConfig{SyncFunc, MaxDelay, MaxBatch}` lacks `OnDegraded` / `Metrics` callbacks — T3b mini-plan decides if `OnDegraded` is needed for Recovery health event emission. `Submit` V3 equivalent may be `SyncCache` (135) — sw VERIFY T3a.
|
||||
|
||||
#### 10.3.7 `write_gate.go` (sw §3.7)
|
||||
|
||||
Both NEVER rows ✅ MATCHES (absence):
|
||||
- `grep writeGate` in `core/storage/` — **zero matches**
|
||||
- `grep ErrNotPrimary|ErrEpochStale|ErrLeaseExpired` in `core/storage/` — **zero matches**
|
||||
|
||||
Authority-surface errors correctly absent from storage. No violation.
|
||||
|
||||
### 10.4 Walk-through summary
|
||||
|
||||
| Metric | Count |
|
||||
|---|---|
|
||||
| Total rows audited | 59 |
|
||||
| ✅ MATCHES | 48 |
|
||||
| ✅ MATCHES-BETTER (V3 cleaner than sw spec) | 2 (compareAndDelete split; no PostSyncCheck field) |
|
||||
| ⚠️ DRIFT acceptable / VERIFY (sw spot-check T3a) | 5 (ScanFrom, Range, snapshot LSN-sort, Submit, DurabilityMode subset) |
|
||||
| ⚠️ DRIFT needing T3b decision | 1 (GroupCommitterConfig OnDegraded) |
|
||||
| ❌ GAP blocking | **1 — `superblock.Epoch` absent (see §10.5)** |
|
||||
| ❌ GAP deferrable | 2 (Metrics callbacks; ExpandEpoch) |
|
||||
| 🚫 VIOLATION | 0 |
|
||||
|
||||
### 10.5 Blocking gap: `superblock.Epoch` absent
|
||||
|
||||
Sw §3.1 classified `Superblock.Epoch` as PORT-REBIND (observation-only). V3 dropped the field entirely. Architecturally clean (storage can't accidentally advance what it doesn't store) but creates gap: T3b Recovery "compare local superblock.epoch vs assigned epoch" has no local source.
|
||||
|
||||
**Resolution options** (T3b mini-plan decides):
|
||||
|
||||
- **Option 1**: T3b adds `Epoch uint64` field to V3 `superblock` as observation-only (~10 LOC + godoc)
|
||||
- **Option 2**: Use `WALCheckpointLSN` as comparison proxy (requires authority to publish LSN watermarks)
|
||||
- **Option 3**: No local epoch mirror; per-I/O adapter fence check (G-int.2) is authoritative; Recovery always reports ready if reload clean
|
||||
|
||||
**QA recommendation: Option 3**. Per-I/O adapter fence check already preserves `INV-FRONTEND-002.*` facet rows. Local epoch in superblock is redundant. Zero LOC added to storage layer. Decision deferred to T3b mini-plan; does NOT block T3.0.
|
||||
|
||||
### 10.6 Walk-through complete — T3.0 Q1 ready
|
||||
|
||||
Q1 (§13) **LOCKED**. V3 existing-port validation:
|
||||
- 48 MATCHES / 2 MATCHES-BETTER / 6 DRIFT-or-VERIFY / 3 GAP / 0 VIOLATION
|
||||
- Phase 08 lesson correctly applied (flusher uses compareAndDelete)
|
||||
- writeGate correctly absent
|
||||
- G-int.6 row 1 (WAL-pressure 1:1) + row 2 (checkpoint-LSN write-back) CONFIRMED
|
||||
- Blocking gap (superblock.Epoch) has QA-recommended Option 3 resolution deferred to T3b
|
||||
|
||||
**QA side ✅ ready to single-sign T3.0**. Awaiting sw confirmation that §14.1 sw-side sanity pass over §10.3 is clean.
|
||||
|
||||
### 10.7 sw sanity-pass comments (2026-04-22, recorded for closure report)
|
||||
|
||||
**A. GroupCommitterConfig `OnDegraded` direction (per §10.3.6 DRIFT)**
|
||||
|
||||
sw preferred direction, locked here for T3b mini-plan:
|
||||
|
||||
> V3's existing "fsync error → returned to waiters via pending channels → propagates up to `WALStore.Write` caller" is a cleaner coupling than V2's `OnDegraded` fan-out. The missing piece is the higher-layer observer (adapter or host) watching error rates and firing local health events. Recommendation: **do NOT reinstate `OnDegraded` on GroupCommitterConfig**; keep storage pure-mechanism; put degraded-signal derivation at the adapter/host layer where it has Identity + ProjectionView context.
|
||||
|
||||
T3b mini-plan adopts this direction unless QA surfaces a counter-argument during T3b sign.
|
||||
|
||||
**B. `superblock.Epoch` framing for closure report (per §10.5 Option 3)**
|
||||
|
||||
sw note on §3.1 row vs §10.3 walk-through:
|
||||
|
||||
> My sw §3.1 row said `Superblock.Epoch` PORT-REBIND (persist but rebind semantic). QA §10.3 found V3 doesn't have the field at all. That's a **stronger §3.2 outcome** than my original proposal — storage can't accidentally misuse epoch because it's not there. The "local epoch vs assigned epoch startup comparison" in §G-int.5 becomes unnecessary; adapter's per-I/O lineage check is the only authority-drift guard. Both rows can stand (mine = "what sw proposed", QA's = "what V3 implements and why it's stronger"); note in T3 closure report so future agents don't re-introduce Epoch thinking it was dropped by accident.
|
||||
|
||||
T3 closure report §B must include this framing: "V3 `superblock` intentionally has no `Epoch` field; per-I/O adapter fence check is sole authority-drift guard; re-introducing an Epoch field to storage is a §8C.3 trigger #4 violation absent a new Discovery Bridge".
|
||||
|
||||
### 10.8 Addendum A — Pre-T3a-open additions (PM direction 2026-04-22)
|
||||
|
||||
PM added two concrete T3 requirements after T3.0 sign, pre-T3a-open. Recorded here (sign holds; these are scope additions to T3a and T3c, not modifications to the §10.3 audit verdicts):
|
||||
|
||||
**Addition 1: Both `walstore` and `smartwal.Store` must be tested**
|
||||
|
||||
Rationale: the two `LogicalStorage` implementations are interchangeable by interface design; production prefers `smartwal.Store`, but `walstore` must keep working as a fallback + comparison baseline. Tests that exercise only one variant leave the other unguarded against silent regression.
|
||||
|
||||
Scope:
|
||||
- **T3a adapter unit tests**: parameterize over both impls (test table with `LogicalStorage` constructor factory), assert adapter behaves identically against both
|
||||
- **T3c scenarios**: each of the 4 scenario YAMLs (crash-recovery / fsync-boundary / disk-fill / wal-replay) runs against both impls — either duplicated YAMLs (`*-walstore.yaml` / `*-smartwal.yaml`) or single YAML with a variant axis in the Go replay
|
||||
- **T3c `DurableProvider` smoke**: Provider opens either impl based on a selector (production default = `smartwal`); smoke tests both selector paths
|
||||
|
||||
Locked constraint: if any single scenario passes on one impl but fails on the other, T3c closure does NOT pass. Variant skew is a regression.
|
||||
|
||||
**Addition 2: Superblock records impl kind + version (storage-layer self-identification)**
|
||||
|
||||
Rationale: "prod 如果能用 smart 最好" + ops/tooling need ability to inspect storage and know what they're looking at without guessing. Impl-kind + version in the superblock enables:
|
||||
- Fail-fast if `DurableProvider` opens with wrong impl selector vs stored format (catches config drift)
|
||||
- Future format migrations know the starting point
|
||||
- Diagnostic tools (`blockvol inspect`) report impl identity
|
||||
|
||||
Scope (T3a):
|
||||
- Add two fields to `core/storage/superblock.go` `superblock` struct:
|
||||
- `ImplKind uint8` (enum: 0=unassigned/error, 1=walstore, 2=smartwal)
|
||||
- `ImplVersion uint32` (per-impl internal schema version)
|
||||
- Both fields set at create (`newSuperblock` callers pass them)
|
||||
- Both fields validated on `readSuperblock`; unknown `ImplKind` → `ErrInvalidSuperblock`
|
||||
- Superblock on-disk layout version constant `CurrentVersion` bumps by 1 (on-disk format change — existing tests of old superblock payload need a one-shot migration or are re-created)
|
||||
|
||||
Classification under §1 framework (addendum to §3.1):
|
||||
- `ImplKind` + `ImplVersion` fields: **NEW (V3-native)** — not in V2; no conflict with PCDD-STUFFING-001 (these identify local mechanism, not authority)
|
||||
- No re-classification of existing §3.1 rows
|
||||
|
||||
Locked: T3a delivers Addition 2 with superblock schema bump; Addition 1 test coverage matrix lands through T3a + T3c.
|
||||
|
||||
**Ledger row addition** (queued for T3a close):
|
||||
- `INV-DURABLE-IMPL-IDENTITY-001` — Superblock records `ImplKind` + `ImplVersion`; `DurableProvider.Open` mismatch-detection rejects with a clear error, not silent coercion
|
||||
|
||||
**Impact on T3a scope + LOC estimate**:
|
||||
- T3a prod LOC: +30 (superblock field extension + validation) + ~50 (matrix test parameterization plumbing) = 400-550 → 480-630
|
||||
- T3a test LOC: +100 (per-impl test table) = 250-300 → 350-400
|
||||
- T3c test LOC: +200 (scenarios replayed over two impls) = 500-700 → 700-900
|
||||
- Wall-clock: +0.5 day in T3a + 0.5 day in T3c = net +1 day
|
||||
|
||||
**Impact on T3.0 sign**: none. Additions are T3a/T3c scope; T3.0 single-sign per §8C.2 still ✅ valid. Documented here for T3a mini-plan referencee.
|
||||
|
||||
---
|
||||
|
||||
## 11. Integration gap analysis (what's missing BEYOND the 7 muscle files)
|
||||
|
||||
Even after §10 validation passes, V3 reaching G4 needs integration layer NOT in `core/storage/`:
|
||||
|
||||
### G-int.1 — Interface bridge: `LogicalStorage` → `frontend.Backend`
|
||||
|
||||
V3 `core/frontend/types.go`:
|
||||
```go
|
||||
type Backend interface {
|
||||
Read(ctx, offset int64, p []byte) (int, error)
|
||||
Write(ctx, offset int64, p []byte) (int, error)
|
||||
Identity() Identity
|
||||
Close() error
|
||||
}
|
||||
```
|
||||
|
||||
V3 `core/storage/LogicalStorage`:
|
||||
```go
|
||||
Write(lba uint32, data []byte) (lsn uint64, err error)
|
||||
Read(lba uint32) ([]byte, error)
|
||||
Sync() (stableLSN uint64, err error)
|
||||
Recover() (recoveredLSN uint64, err error)
|
||||
Boundaries() (R, S, H uint64)
|
||||
NextLSN() uint64
|
||||
```
|
||||
|
||||
Gap: byte↔LBA translation, ctx threading, Sync policy (see G-int.2).
|
||||
|
||||
### G-int.2 — Sync policy DECISION: explicit `Backend.Sync()` (sw addition A)
|
||||
|
||||
- iSCSI emits `SYNCHRONIZE CACHE` (SPC-5 §7.26); NVMe emits `Flush` opcode
|
||||
- Auto-sync-per-Write kills IOPS perf. **Rejected.**
|
||||
- **Locked**: extend `frontend.Backend` interface with `Sync(ctx) error`. T3a adapter calls `LogicalStorage.Sync()`. T3b wires iSCSI `SYNC_CACHE` handler + NVMe `Flush` handler to `Backend.Sync(ctx)`.
|
||||
|
||||
### G-int.3 — Operational gate API LOCKED signature (sw granularity alignment)
|
||||
|
||||
Prevents BUG-001-class fragmentation where T3a adapter ships without the gate API and T3b retrofits. API:
|
||||
|
||||
```go
|
||||
// core/frontend/types.go — additional Backend method
|
||||
SetOperational(ok bool, evidence string)
|
||||
|
||||
// core/frontend/durable/storage_adapter.go
|
||||
type StorageBackend struct {
|
||||
storage storage.LogicalStorage
|
||||
identity frontend.Identity
|
||||
lineageSrc frontend.ProjectionView // current authority lineage
|
||||
blockSize uint32
|
||||
|
||||
operational atomic.Bool
|
||||
nonopEvidence atomic.Value // string
|
||||
}
|
||||
|
||||
// SetOperational flips the backend between "operational" (accepts I/O)
|
||||
// and "not operational" (all I/O returns ErrNotReady).
|
||||
//
|
||||
// Invariant: before first SetOperational(true, _) call, every I/O
|
||||
// returns frontend.ErrNotReady.
|
||||
//
|
||||
// Invariant: SetOperational does NOT touch identity/lineage/epoch;
|
||||
// it is pure local-state readiness. Authority publication stays on
|
||||
// the master path (PCDD-STUFFING-001).
|
||||
func (b *StorageBackend) SetOperational(ok bool, evidence string)
|
||||
|
||||
func (b *StorageBackend) operationalGate() error {
|
||||
if !b.operational.Load() {
|
||||
return fmt.Errorf("%w: %s", frontend.ErrNotReady, b.nonopEvidence.Load())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Read/Write/Sync each call operationalGate() first, then lineage
|
||||
// check (captures Identity at Open; compares to ProjectionView on
|
||||
// each call; mismatch → ErrStalePrimary), then the storage call.
|
||||
```
|
||||
|
||||
**This signature is LOCKED at T3.0 sign.** T3a ships full adapter surface. T3b wires the gate without retrofit.
|
||||
|
||||
### G-int.4 — `DurableProvider` (sw addition E: testback stays for tests)
|
||||
|
||||
New `core/frontend/durable/provider.go` implements `frontend.Provider` for production (`cmd/blockvolume`). `core/frontend/testback.StaticProvider` unchanged — unit tests keep injection path.
|
||||
|
||||
### G-int.5 — Recovery integration (PM fix #1 read-side only)
|
||||
|
||||
Verified wire point: `core/host/volume/healthy_executor.go` exists with `SetOnSessionStart / SetOnSessionClose / SetOnFenceComplete / Probe / StartCatchUp` hooks. Recovery plugs in via these — no new publication path.
|
||||
|
||||
Sequence:
|
||||
```
|
||||
cmd/blockvolume startup:
|
||||
1. Open LogicalStorage (walstore or smartwal per config)
|
||||
2. LogicalStorage.Recover() → (recoveredLSN, err)
|
||||
3. Read superblock.epoch + compare to Identity from assignment
|
||||
3a. If local epoch > assigned epoch:
|
||||
→ adapter.SetOperational(false, "local epoch ahead of assignment")
|
||||
→ report NotReady via status_server.go /status (read-side; no publish)
|
||||
3b. If local epoch ≤ assigned epoch:
|
||||
→ adapter.SetOperational(true, "recovered LSN=X epoch=Y")
|
||||
→ HealthyPathExecutor.Probe proceeds (T1 path)
|
||||
4. Backend accepts I/O
|
||||
```
|
||||
|
||||
### G-int.6 — sw addition C: 5-item V2 flusher drift-confirm
|
||||
|
||||
Beyond the 7 muscle files in sw's §3, sw flagged 5 V2 flusher behaviors needing drift-confirm against V3:
|
||||
|
||||
| V2 item | V3 substitute | Verdict (sw LOCKED 2026-04-22) |
|
||||
|---|---|---|
|
||||
| V2 WAL-pressure exits (`ErrWALFull` / `ErrVolumeClosed`) via `wal_admission.Acquire` | V3 `core/storage/wal_admission.go:110 errWALFull`, `:104 errVolumeClosed`, `:146 errWALFull`, `:149 errVolumeClosed` | ✅ **COVERED — LOCKED**. Exit-code 1:1 map confirmed; V3 uses unexported `err*` names (package-private by design; callers go through `WALStore.Write` which wraps). Control flow in V3 `wal_admission.go:90-156 Acquire` is structurally identical to V2: hard-watermark spinwait → soft-watermark scaled delay → semaphore acquisition. `PressureState()` returns the same `"hard"` / `"soft"` / `"normal"` trichotomy (V3 line 71, V2 line 67). |
|
||||
| V2 `updateSuperblockCheckpoint / CheckpointLSN / SetCheckpointLSN` (public setter) | V3 `walstore.go:243 persistCheckpoint(highestLSN)` called by `flusher.go:226` after extent writes; V3 `walstore.go:272 CheckpointLSN()` public getter; V3 `walstore.go:260-266` re-encodes superblock + pwrite@0 | ✅ **COVERED — LOCKED**. Public getter + on-disk persistence wired. Note: V3 intentionally removes V2's public `SetCheckpointLSN` setter — checkpoint advance in V3 is *only* through the flusher path (one write authority). Sw classifies this drop as **safety improvement, not drift** — prevents external munging of checkpoint position. Any future caller that needs to force checkpoint must go through flusher hooks, not a direct setter. |
|
||||
| `RetentionFloorFn / SetRetentionFloorFn` | None in V3 | ⏸ **DEFER-TO-T4 — LOCKED**. Rebuild retention is rebuild-layer concern; rebuild OUT of T3 per §G-int.7. When rebuild path lands (T4 or later), retention floor becomes a REINSTATE item against that batch's audit — NOT against T3. |
|
||||
| `AddSnapshot / PauseAndFlush / Pause / Resume` | None in V3 | ⏸ **DEFER-TO-Tn — LOCKED**. Snapshot export + rebuild-gate coordination both OUT of T3. Same treatment as retention floor: reinstate with the owning batch's audit. |
|
||||
| V2 `Flusher.SetFD` | V3 `walstore.go:131 OpenWALStore(path)` + `walstore.go:156 openInitialized(path, fd, sb)` | ✅ **COVERED — LOCKED**. V2 `flusher.go:524 SetFD` is explicitly marked test-only ("Test-only"). V3 achieves the same test-injection surface via the path-based `OpenWALStore` (production) + `openInitialized` (test; callers pass `*os.File` + pre-validated superblock). Production move-reopen pattern in V3 is `Close()` → `OpenWALStore(path)` — no FD-swap hook needed in production. |
|
||||
|
||||
**All 5 rows LOCKED 2026-04-22 by sw.** Zero REINSTATE items → T3a LOC budget unchanged. Two DEFER rows tracked against future batches per §G-int.7 rebuild-OUT discipline.
|
||||
|
||||
### G-int.7 — sw addition D: Rebuild OUT of T3 (explicit)
|
||||
|
||||
Rebuild-time durability (`ReplicaRebuilding` state, WAL retention-floor coordination surfaced during BUG-001) is **OUT of T3**. Any T3c scenario touching rebuild → §8C.3 trigger #3 (cross-T pollution) escalate.
|
||||
|
||||
---
|
||||
|
||||
## 12. Batch split (LOCKED)
|
||||
|
||||
Per sw granularity alignment to prevent BUG-001-class fragmentation:
|
||||
|
||||
| Batch | Content | Gate | Sign |
|
||||
|---|---|---|---|
|
||||
| **T3.0** | This doc. Sw §1–§9 V2 spec + QA §10.1 validation + §11 integration gaps + G-int.3 operational-gate API lock + G-int.6 drift-confirm LOCKED | No code; sign-gate | QA single-sign after sw flips §11 G-int.6 DRAFT→LOCKED + QA completes §10.1 walk-through |
|
||||
| **T3a** | G-int.1 + G-int.2 + G-int.3 — complete `core/frontend/durable/storage_adapter.go` + Backend interface extension (Sync + SetOperational) + testback shim updates | Unit A (adapter) | QA single-sign §8C.2 |
|
||||
| **T3b** | G-int.4 Provider + G-int.5 Recovery integration + iSCSI SYNC_CACHE wire + NVMe Flush wire | Units B+C (provider + recovery) | QA single-sign §8C.2 |
|
||||
| **T3c** | 4 scenarios (crash-recovery / fsync-boundary / disk-fill / wal-replay) + iSCSI/NVMe continuity smoke + perf baseline artifact + T3 closure report | Acceptance | architect + PM + QA three-sign §8C.1 |
|
||||
|
||||
Wall-clock: T3.0 half-day + T3a 1-1.5 day + T3b 1-1.5 day + T3c 1-2 day = **4-5.5 days**.
|
||||
|
||||
LOC (revised):
|
||||
|
||||
| Batch | Prod LOC | Test LOC |
|
||||
|---|---|---|
|
||||
| T3.0 | 0 | 0 |
|
||||
| T3a | 400-550 | 250-300 |
|
||||
| T3b | 350-450 | 250-300 |
|
||||
| T3c | 0-50 | 500-700 |
|
||||
| **Total** | **~800-1050** | **~1000-1300** |
|
||||
|
||||
---
|
||||
|
||||
## 13. Open questions (pre-T3a blockers)
|
||||
|
||||
| # | Question | Owner | Blocks | Status |
|
||||
|---|---|---|---|---|
|
||||
| Q1 | §10.1 V3 existing-port validation walk-through complete | QA | T3.0 sign | ⏸ in progress |
|
||||
| Q2 | G-int.6 row 1 (WAL-pressure exit-code 1:1 map) LOCKED | sw | T3.0 sign | ✅ **LOCKED 2026-04-22** (V3 `wal_admission.go:104/110/146/149`) |
|
||||
| Q3 | G-int.6 row 2 (checkpoint-LSN write-back path wired) LOCKED | sw | T3.0 sign | ✅ **LOCKED 2026-04-22** (V3 `walstore.go:243 persistCheckpoint` called by `flusher.go:226`) |
|
||||
| Q4 | G-int.6 row 5 (`SetFD` covered by `OpenWALStore/Recover`) LOCKED | sw | T3.0 sign | ✅ **LOCKED 2026-04-22** (V3 `walstore.go:131 OpenWALStore` + `:156 openInitialized`; V2 SetFD was test-only) |
|
||||
| Q5 | Default WAL impl (smartwal vs walstore) for production `DurableProvider` | sw + QA joint | T3b mini-plan | ⏸ open |
|
||||
| Q6 | `superblock.epoch` write policy — only on fence advance via adapter, or every checkpoint? | sw + QA joint | T3b mini-plan | ⏸ open |
|
||||
|
||||
Q2-Q4 ✅ sw side cleared. Q1 awaits QA §10.1 walk-through. Q5-Q6 block T3b open (not T3.0).
|
||||
|
||||
---
|
||||
|
||||
## 14. Updated sign-off
|
||||
|
||||
### 14.1 T3.0 sign table (single-sign per §8C.2)
|
||||
|
||||
| Role | Signer | Date | Prerequisite | Status |
|
||||
|---|---|---|---|---|
|
||||
| sw | Claude (sw agent) | 2026-04-22 | Flips G-int.6 Q2/Q3/Q4 DRAFT → LOCKED; sanity-passes §10.3 | ✅ **Q2/Q3/Q4 LOCKED**; §10.3 sanity ✅ confirmed (5/5 findings verified) + sw-side comments A (OnDegraded direction) + B (Epoch gap framing) recorded in §10.7 |
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | §10.3 walk-through LOCKED (§10.6); Option 3 recommended for Epoch gap (deferred to T3b) | ✅ **Q1 LOCKED**; T3.0 **SIGNED per §8C.2 single-sign** |
|
||||
|
||||
**Effect upon sign**: T3a opens. sw implements against §11 G-int.3 operational-gate API signature + Backend interface extension in §11 G-int.1/G-int.2. QA files T3a mini port plan (expected ~50 lines; single-signed) before code commit.
|
||||
|
||||
### 14.2 Ledger row plan
|
||||
|
||||
Rows queued ACTIVE at respective batch close:
|
||||
|
||||
| ID | Statement | Added at |
|
||||
|---|---|---|
|
||||
| `INV-DURABLE-001` | Acknowledged Write survives kill + restart; Read returns byte-exact | T3c crash-recovery scenario |
|
||||
| `INV-DURABLE-WAL-REPLAY-001` | N acked writes + crash → all N recoverable | T3c wal-replay scenario |
|
||||
| `INV-DURABLE-FSYNC-BOUNDARY-001` | Flushed preserved; unflushed may be lost; neither corrupted | T3c fsync-boundary scenario |
|
||||
| `PCDD-DURABLE-DISK-FULL-001` | Disk-full → hard error; no silent success | T3c disk-fill scenario |
|
||||
| `INV-DURABLE-EPOCH-PERSISTED-001` | Epoch persists through superblock; restart rejects stale-lineage | T3b recovery test + T3c |
|
||||
| `INV-DURABLE-OPGATE-001` | Before `SetOperational(true)`, all I/O → `ErrNotReady`; preserves `INV-FRONTEND-002.*` under durable | T3a adapter unit test |
|
||||
@@ -0,0 +1,243 @@
|
||||
# V3 Phase 15 T3 — Local Durable Data Path (G4) — Port Plan Sketch
|
||||
|
||||
**Date**: 2026-04-22 (rev-2 post PM/architect review)
|
||||
**Status**: **DRAFT v2 — external review** (PM / architect). Major rewrite from v1 to align with canonical P15 roadmap.
|
||||
**Author**: QA Owner (rev-2)
|
||||
**Predecessor**: T2 closed (Gate G3) 2026-04-22
|
||||
**Gate**: **G4 Local Durable Data Path**, per `v3-phase-15-mvp-scope-gates.md` §G4 + `v3-phase-15-product-plan.md` §T3
|
||||
**Governing methodology**: §8B PCDD + §8C accelerated-cadence governance
|
||||
|
||||
---
|
||||
|
||||
## 0. For the reviewer (read first)
|
||||
|
||||
**What this doc is**: T3 scope + port plan. T3 = **G4 Local Durable Data Path** — V3 frontend I/O reaches real local block storage with durable write/recovery semantics.
|
||||
|
||||
**What's already decided (no review needed)**:
|
||||
- T2 closed 2026-04-22 (frontend protocols + product attach proven end-to-end)
|
||||
- §8C Accelerated-Cadence Governance active (mid-T QA-solo; T-boundary three-sign)
|
||||
|
||||
**What needs reviewer decision** (only these):
|
||||
- §3 IN-list aligns with canonical 7-file V2 port list + 2 new components
|
||||
- §4 OUT-list preserves P15 track separation (no replication / no failover / no security spill-in)
|
||||
- §7 A-tier invariants (5 new ledger rows, named under `INV-DURABLE-*` / `PCDD-DURABLE-*` per canonical ledger taxonomy)
|
||||
- §11 open questions + QA defaults
|
||||
- §12 signatures
|
||||
|
||||
**v1 → v2 rewrite summary (what changed vs previous sketch)**:
|
||||
- v1 framed T3 as "Perf Gate + Mechanism Debt Paydown" — this was scope drift; the canonical P15 roadmap defines T3/G4 as Local Durable Data Path. v2 corrects this.
|
||||
- Perf content moved from "gate" to "characterization smoke only, NOT a pass/fail gate" (per review: formal perf SLO belongs at G21/T8 or Final Gate)
|
||||
- `L1B-2 FailoverMidWrite` removed from IN-list (canonical: G8/T6)
|
||||
- `Prometheus metrics export` removed from production deliverable (canonical: G17/T8 diagnostics)
|
||||
- Ledger row names corrected: `PCDD-PERF-*` → `INV-DURABLE-*` / `PCDD-DURABLE-*`
|
||||
|
||||
See §13 Change log + `v3-phase-15-t3-sketch-v1-retraction.md` for the v1 audit trail.
|
||||
|
||||
---
|
||||
|
||||
## 1. T3 Purpose — One Sentence
|
||||
|
||||
Deliver real local durable storage behind the V3 frontend — Write / Read / Flush / Close survive a crash and restart, with ack'd data recoverable byte-exact.
|
||||
|
||||
## 2. Gate Name
|
||||
|
||||
**G4 — Local Durable Data Path**. Exact name from `v3-phase-15-mvp-scope-gates.md` §G4. No invention.
|
||||
|
||||
## 3. Scope (IN-list)
|
||||
|
||||
### 3.1 V2 port (canonical 7 files per `v3-phase-15-mvp-scope-gates.md` §G4 + `v3-phase-15-product-plan.md` §T3)
|
||||
|
||||
| # | V2 file | Category | Scope |
|
||||
|---|---|---|---|
|
||||
| 1 | `weed/storage/blockvol/logical_storage*.go` | M | Port — V3's per-volume durable storage abstraction |
|
||||
| 2 | `weed/storage/blockvol/smartwal*.go` | M | Port — WAL format + append + replay |
|
||||
| 3 | `weed/storage/blockvol/wal_*.go` | M | Port — WAL writer, admission, pressure handling |
|
||||
| 4 | `weed/storage/blockvol/dirty_map.go` | M | Port — page-dirty tracking for recovery |
|
||||
| 5 | `weed/storage/blockvol/group_commit.go` | M | Port — batched fsync coordinator |
|
||||
| 6 | `weed/storage/blockvol/write_gate.go` | M | Port — admission gate tied to fencing |
|
||||
| 7 | `weed/storage/blockvol/superblock.go` | M | Port — volume metadata + epoch persistence |
|
||||
|
||||
Port discipline: **port-model faithful** per `v3-phase-15-t2-port-model-decision.md` §D2. No pragmatic simplification. V2 behaviors + invariants preserved; rebind storage IO calls to V3's backend interface.
|
||||
|
||||
**Pre-code T3.0 function-level port audit (mandatory before any T3 code commit)**:
|
||||
|
||||
Each of the 7 M-category files contains a mix of pure-mechanism code and V2-specific authority/storage assumptions. `write_gate.go`, WAL-pressure handling paths, and `superblock.go` epoch fields in particular may carry V2 behaviors that don't rebind 1:1 to V3 (e.g., V2 epoch was sometimes locally advanced; V3 authority is master-published only — `PCDD-STUFFING-001`). Before sw writes the first line of T3 production code, sw produces `sw-block/design/v3-phase-15-t3-port-audit.md` classifying each function in the 7 files as one of:
|
||||
|
||||
| Verdict | Meaning | Required annotation |
|
||||
|---|---|---|
|
||||
| `PORT-AS-IS` | Pure mechanism; copy verbatim, adjust imports only | — |
|
||||
| `PORT-REBIND` | Mechanism + V2-specific call-site; port logic, rebind call-site to V3 equivalent | Explicit rebind target (V3 type / method / config) |
|
||||
| `DEFER` | Mechanism not required for G4 correctness; postpone | Target track (T4 / G21 / etc.) |
|
||||
| `NEVER` | Contains authority-as-storage mixing, retry-as-authority, or other E-category pattern | Citation of stop rule violated |
|
||||
|
||||
QA reviews the audit before signing T3a mini port plan. This is the dual-line port discipline (§8B.2 port-model + §8C.3 trigger #4 no pragmatic evolution) applied at function granularity.
|
||||
|
||||
### 3.2 New V3 components (per canonical §G4 "Make new")
|
||||
|
||||
1. **V3 local storage adapter with epoch/session/fence awareness** — connects ported V2 durable engine to V3 `frontend.Backend` + authority lineage (Epoch / EndpointVersion / ReplicaID / Healthy). Stale-lineage writes/reads return `ErrStalePrimary` (invariant already pinned by `INV-FRONTEND-002.*`; this adapter must preserve the pin under durable backend). **Adapter consumes published authority facts; it does not mint, advance, or publish Epoch / EndpointVersion / assignment — those remain master/authority's sole responsibility (`PCDD-STUFFING-001` boundary).**
|
||||
2. **Recovery integration (read-side only)** — on restart: reload superblock → replay WAL → resync dirty_map → expose **recovered readiness** to the existing volume host / adapter projection path (same surface T1 built). Volume host then goes through its normal `HealthyPathExecutor` flow; master authority / publisher remains the ONLY source of Epoch / assignment publication. If recovered local state disagrees with the master-assigned lineage (e.g., superblock epoch > assigned epoch, or persisted writes beyond assigned EV), the volume reports **NotReady** with diagnostic evidence and refuses to serve I/O until master-side reconciliation. Storage adapter does NOT "publish epoch advance" — it observes, recovers, and reports.
|
||||
|
||||
### 3.3 Scenario coverage (canonical pass-gate translation)
|
||||
|
||||
Canonical pass-gate quoted: *"crash/restart test writes acknowledged data through the real frontend, kills/restarts the local process, and reads the acknowledged data back"*.
|
||||
|
||||
Materialized as scenario YAML + Go replay:
|
||||
|
||||
| ID | Scenario | Purpose |
|
||||
|---|---|---|
|
||||
| `t3-durable-crash-recovery.yaml` | Write 32 KiB at LBA 0 → ack → `SIGKILL` volume → restart → Read LBA 0 returns same bytes | Canonical G4 pass gate |
|
||||
| `t3-durable-fsync-boundary.yaml` | Write unflushed → kill before sync → restart → Read may return pre-write bytes OR post-write bytes (either spec-legal), never corrupted | fsync semantics |
|
||||
| `t3-durable-disk-fill.yaml` | Fill backing store → write → graceful error (no silent success, no corrupt) | fail-hard semantics |
|
||||
| `t3-durable-wal-replay.yaml` | Write N times → kill between writes → restart → first N acked writes recoverable | WAL replay correctness |
|
||||
|
||||
### 3.4 Frontend continuity (from T2)
|
||||
|
||||
iSCSI + NVMe frontends from T2 MUST continue working, now backed by the durable storage (not memback). A T2-style smoke (attach + mkfs + mount + write + sync + umount + disconnect) MUST pass against the durable backend. This is the frontend-to-durable-backend integration, not new frontend surface.
|
||||
|
||||
### 3.5 Perf characterization (smoke only, NOT a gate)
|
||||
|
||||
Per review: T3 is NOT a perf gate. Perf numbers **published as baseline artifact**; no pass/fail threshold. Formal perf SLO lives at G21 / Final Gate.
|
||||
|
||||
Content:
|
||||
- fio harness runs one fixed workload (e.g., 4 KiB random write × 60s) against T3 durable backend
|
||||
- Numbers captured to `t3-perf-baseline.md` as characterization
|
||||
- Optional: same run against T2 memback for comparison context
|
||||
- **No `PCDD-PERF-*` ledger row**; **no V2-vs-V3 threshold**; only "published measurements exist"
|
||||
|
||||
## 4. Scope (OUT-list — explicit)
|
||||
|
||||
| Item | Reason | Where it belongs |
|
||||
|---|---|---|
|
||||
| Replication (WAL shipping, shipper group, replica apply, barrier) | Canonical T4/G5 | T4 per `v3-phase-15-product-plan.md` §T4 |
|
||||
| Rebuild / catch-up | Canonical T5/G6 or G7 | T5 per product plan |
|
||||
| Failover primary move (incl. L1B-2 FailoverMidWrite) | Canonical G8/T6 | G8/T6 |
|
||||
| ALUA / CHAP / multi-path / auth | S-category | T7/T8 per product plan |
|
||||
| Prometheus metrics production export | G17/T8 diagnostics surface | T8 per canonical |
|
||||
| Formal perf SLO / V2 pass/fail threshold | Not a G4 concern; data-path correctness first | G21 / Final Gate |
|
||||
| 24h soak / chaos / endurance | G22 Final Gate | G22 |
|
||||
| CSI driver | Depends on G4 passed | per product plan, CSI follows G3+G4 |
|
||||
| RF-1 bufpool (NVMe) | M\* perf-adjacent; not required for G4 correctness | T3 MAY include if it's blocking a test harness; otherwise defer to T3 perf-smoke follow-up or G21 |
|
||||
|
||||
## 5. V2 port-debt map (M/M\*/E/S per §4A port-migration plan)
|
||||
|
||||
Post-T2, durable-side gap analysis:
|
||||
|
||||
| V2 file | Category | V3 status after T2 | T3 action |
|
||||
|---|---|---|---|
|
||||
| `logical_storage*.go` | M | MISSING | **Port in T3** |
|
||||
| `smartwal*.go` | M | MISSING | **Port in T3** |
|
||||
| `wal_*.go` | M | MISSING | **Port in T3** |
|
||||
| `dirty_map.go` | M | MISSING | **Port in T3** |
|
||||
| `group_commit.go` | M | MISSING | **Port in T3** |
|
||||
| `write_gate.go` | M | MISSING | **Port in T3** |
|
||||
| `superblock.go` | M | MISSING | **Port in T3** |
|
||||
| `adapter.go` (V2 storage glue) | E | Correctly NOT ported in T2 | NOT ported; replaced by new V3 storage adapter (§3.2 item 1) |
|
||||
| `write_retry.go` | E | Correctly NOT ported; pinned by `PCDD-NVME-IO-NO-TARGET-RETRY-001` | NOT ported; confirm `PCDD-NVME-IO-NO-TARGET-RETRY-001` still ACTIVE post-T3 |
|
||||
|
||||
## 6. Stop rules (mechanical)
|
||||
|
||||
1. Boundary guard hard — frontend + storage MUST NOT import `core/authority` or `core/adapter` (PCDD-STUFFING-001 scope extends to new storage package)
|
||||
2. Port-model default for the 7 M files — pragmatic evolution forbidden; any deviation requires written rationale + §8C.3 trigger #4 escalation
|
||||
3. No retry-as-authority (`write_retry.go` stays out) — pinned by existing `PCDD-NVME-IO-NO-TARGET-RETRY-001`
|
||||
4. Advertised ≡ implemented: any new frontend-visible capability advertising durability MUST have an ACTIVE ledger row pinning the durability behavior
|
||||
5. A-tier ledger rows stay GREEN — T2's 16 rows (iSCSI VPD + NVMe Identify/CNTLID/IO-no-retry/etc.) must remain ACTIVE after T3 lands
|
||||
6. No scope creep into replication / failover / security — violations trigger §8C.3 #3 (cross-T pollution)
|
||||
|
||||
## 7. A-tier invariants to add in T3
|
||||
|
||||
Per canonical ledger taxonomy (`INV-*` for positive invariants, `PCDD-*` for named bad-state families):
|
||||
|
||||
| ID (provisional) | Invariant | Layer |
|
||||
|---|---|---|
|
||||
| `INV-DURABLE-001` | Acknowledged Write survives process kill + restart; Read after restart returns byte-exact what was ack'd | Unit + Scenario |
|
||||
| `INV-DURABLE-WAL-REPLAY-001` | N acknowledged writes followed by crash → all N recoverable via WAL replay; no ack'd write lost | Unit + Scenario |
|
||||
| `INV-DURABLE-FSYNC-BOUNDARY-001` | Unflushed Write may be lost on crash (spec-legal); flushed Write MUST be preserved. No partial / corrupted reads on either side of boundary | Unit + Scenario |
|
||||
| `PCDD-DURABLE-DISK-FULL-001` | Backing store exhaustion returns hard error to frontend; no silent success; no corrupted state (dirty_map consistent post-error) | Unit + Scenario |
|
||||
| `INV-DURABLE-EPOCH-PERSISTED-001` | Epoch advances persisted through superblock; restart reloads current epoch, rejects stale-lineage I/O (preserves `INV-FRONTEND-002.EPOCH` under durable backend) | Unit + Component |
|
||||
|
||||
## 8. Acceptance sign checklist (for T3-end)
|
||||
|
||||
| # | Gate | Owner |
|
||||
|---|---|---|
|
||||
| 1 | 5 new A-tier ledger rows ACTIVE (§7) | QA |
|
||||
| 2 | Canonical G4 pass-gate scenario (write / kill / restart / read-back byte-exact) green | sw + QA |
|
||||
| 3 | 4 scenario YAMLs (§3.3) green with Go replay | sw + QA |
|
||||
| 4 | T2 frontend smoke (iSCSI + NVMe) still green against durable backend (§3.4) | QA |
|
||||
| 5 | Full regression green: `go test ./core/...` | both |
|
||||
| 6 | Boundary guard green (incl. new storage package) | both |
|
||||
| 7 | T2's 16 existing ACTIVE ledger rows still ACTIVE | QA |
|
||||
| 8 | Perf baseline published as artifact (`t3-perf-baseline.md`); no threshold | QA |
|
||||
| 9 | Closure report for T3 with ledger rows + commit hashes | QA |
|
||||
| 10 | **T3-end three-sign** per §8C.1 | architect + PM + QA |
|
||||
|
||||
## 9. Estimated size
|
||||
|
||||
| Metric | Estimate |
|
||||
|---|---|
|
||||
| Production LOC delta (ported V2 + new adapter + recovery integration) | ~1500-2500 |
|
||||
| Test LOC delta (unit + scenario YAMLs + replay + frontend continuity) | ~1500-2500 |
|
||||
| Wall-clock | 5-8 work days |
|
||||
| Main risks | (a) V2 durability code has subtle correctness invariants not apparent from top-level shape — faithful port with V2 test suite re-run is load-bearing; (b) V3 adapter must preserve `INV-FRONTEND-002.*` facet rows under durable backend (regression risk) |
|
||||
|
||||
## 10. Inventory items NOT carried into T3
|
||||
|
||||
From `sw-block/design/bugs/inventory/nvme-test-coverage-deferred.md`, the following remain inventoried and are NOT T3 scope:
|
||||
|
||||
- L1B-2 FailoverMidWrite → G8/T6 (failover data continuity track)
|
||||
- L1B-5 MultiVolumeConcurrent → depends on harness change in T3 but the test itself is perf-adjacent; defer to T3-end's optional perf-smoke or G21
|
||||
- All L2-B scenarios except the 4 durability ones in §3.3 → T4+ or G22
|
||||
- L3B / L4B endurance → G22 Final Gate
|
||||
- Replication-related (L1B-2 failover, any HA scenario) → T4+ / G8
|
||||
|
||||
Quarterly rotation per inventory doc §Rotation audit continues.
|
||||
|
||||
## 11. Open questions (QA-default answers below)
|
||||
|
||||
| # | Question | Reviewer decision needed? |
|
||||
|---|---|---|
|
||||
| 1 | Perf characterization workload — single fio pattern or small matrix? | Yes |
|
||||
| 2 | Baseline hardware for perf smoke — m01 + M02 only? | Yes |
|
||||
| 3 | V2 durability behavior vs V3: where can V3 legitimately diverge? (e.g., V2 had specific retry-within-WAL behavior that overlaps with `write_retry.go` — must confirm that part is NOT ported) | Yes |
|
||||
| 4 | Recovery integration with epoch advance — who publishes post-recovery epoch? (new adapter's responsibility vs an existing publication path) | Yes |
|
||||
|
||||
### 11.5 QA default answers (accept by not editing; override per-line if you disagree)
|
||||
|
||||
| # | Question | QA-default |
|
||||
|---|---|---|
|
||||
| 1 | Perf workload | Single fio pattern: 4 KiB random write, 60s, 8 IO depth. One number to publish. Matrix is G21 concern. |
|
||||
| 2 | Baseline hardware | m01 + M02 for T3; second-class hardware at G22. |
|
||||
| 3 | V2 durability / `write_retry.go` boundary | `write_retry.go` stays NOT ported (E-category, pinned by `PCDD-NVME-IO-NO-TARGET-RETRY-001`). Any retry-looking code inside the 7 M files (e.g., WAL admission-retry-on-pressure) is port-model-literal: ported if it's part of the ack path; rejected if it's authority-layer retry-as-authority. Concrete file-by-file triage done during port audit (sw produces during T3 kickoff). |
|
||||
| 4 | Recovery → readiness path (NOT epoch publish) | **Storage adapter does NOT publish epoch.** On restart, durable storage reloads superblock/WAL and exposes **recovered readiness** (a local-state signal, not an authority fact) to the existing volume host / adapter path built in T1. Epoch / EndpointVersion remain master-published authority facts (`PCDD-STUFFING-001`). If recovered local state disagrees with assigned lineage (e.g., superblock epoch > assigned epoch), volume reports `NotReady` with evidence; master-side reconciliation governs. Storage layer observes + recovers + reports; it does not mint, advance, or publish authority. |
|
||||
|
||||
## 12. Change log + signatures
|
||||
|
||||
### 12.1 T3-start signatures (required for sw to begin coding)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| Architect | pingqiu (acting) | 2026-04-22 | ✅ approved rev-2 pending 2 fixes (applied); fixes landed in rev-2.1 |
|
||||
| PM | pingqiu (acting) | 2026-04-22 | ✅ approved rev-2 pending 2 fixes (applied); fixes landed in rev-2.1 |
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ signed rev-2.1 post-fixes |
|
||||
|
||||
Once all three sign, sw + QA execute T3 per §8C (mid-T QA-solo signing). T3-end returns to three-sign.
|
||||
|
||||
### 12.2 Change log
|
||||
|
||||
| Date | Change | Author |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | Initial sketch (v1) — proposed "Perf Gate + Mechanism Debt Paydown" framing | QA Owner |
|
||||
| 2026-04-22 | **v1 REJECTED** by PM/architect review: canonical T3/G4 is Local Durable Data Path, not perf. v1 was scope drift. Specific findings: perf-as-gate wrong position, missing 7 V2 durable files, L1B-2 FailoverMidWrite belongs to G8/T6 not T3, Prometheus metrics belongs to G17/T8, ledger row names `PCDD-PERF-*` incorrect taxonomy. | PM/architect |
|
||||
| 2026-04-22 | v2 rewrite per review: gate identity restored to **G4 Local Durable Data Path**; 7 V2 files listed per canonical `v3-phase-15-mvp-scope-gates.md` §G4; perf demoted from gate to smoke-only characterization (no threshold); L1B-2 + Prometheus removed; ledger rows renamed `INV-DURABLE-*` / `PCDD-DURABLE-*`; §11 open questions aligned with durable semantics | QA Owner |
|
||||
| 2026-04-22 | **v2.1 fixes per PM/architect rev-2 review (signable)**: (a) §3.2 + §11.5 #4 corrected — storage adapter does NOT publish epoch; recovery exposes readiness only; master-published authority is sole source of Epoch/EV per `PCDD-STUFFING-001`. (b) §3.1 added mandatory **T3.0 function-level port audit** prerequisite — sw classifies each function in the 7 M files as PORT-AS-IS / PORT-REBIND / DEFER / NEVER before any code commit. T3-start three-sign complete. | PM/architect + QA Owner |
|
||||
|
||||
---
|
||||
|
||||
## 13. Governance lesson (for audit trail)
|
||||
|
||||
v1 is a case study for **§8C Rule**: *"accelerated cadence speeds up internal decisions (who reviews, how many signs), but the canonical delivery scope is immutable — review must enforce alignment with the roadmap."* QA Owner's first draft drifted T3 from Local Durable Data Path (canonical) to Perf Gate (invented). PM/architect review caught it. The lesson:
|
||||
|
||||
- QA Owner has mid-T single-sign authority under §8C.2 for **implementation decisions** (test shapes, bug fix bundling, port-model vs pragmatic-evolution calls inside the ported files)
|
||||
- QA Owner does NOT have authority to redefine track identity / gate definition / canonical V2 port sources
|
||||
- T-boundary three-sign exists specifically to catch this drift; it worked as designed
|
||||
|
||||
No rule change needed — §8C.1 three-sign-at-T-boundaries is already the enforcing mechanism. This doc (§13) is the audit record.
|
||||
@@ -0,0 +1,147 @@
|
||||
# V3 Phase 15 T3a — Mini Port Plan
|
||||
|
||||
**Date**: 2026-04-22
|
||||
**Status**: QA single-signed per §8C.2; sw cleared to start
|
||||
**Owner**: QA drafts + signs; sw implements
|
||||
**Predecessor**: T3.0 port audit SIGNED (`v3-phase-15-t3-port-audit.md` incl. §10.8 Addendum A)
|
||||
**Gate**: T3a — adapter + Backend interface extension + superblock impl-identity fields
|
||||
**Successor**: T3b mini-plan (post T3a close)
|
||||
**Governing methodology**: §8B PCDD + §8C accelerated-cadence governance (mid-T QA single-sign)
|
||||
|
||||
---
|
||||
|
||||
## 0. Scope (locked)
|
||||
|
||||
T3a delivers 4 things in one commit:
|
||||
|
||||
1. **G-int.1 + G-int.2 + G-int.3** — `core/frontend/durable/storage_adapter.go` with full surface: byte↔LBA translation, per-I/O fence check (preserves `INV-FRONTEND-002.*`), operational gate (`SetOperational`), and `Sync` implementation via `LogicalStorage.Sync`.
|
||||
2. **Backend interface extension** — `core/frontend/types.go` gains `Sync(ctx) error` + `SetOperational(ok bool, evidence string)`. All existing `Backend` implementations (memback testbacks, iSCSI/NVMe harness backends) gain trivial impls.
|
||||
3. **Addendum A Addition 2** — `core/storage/superblock.go` gains `ImplKind` + `ImplVersion` fields; `newSuperblock` accepts them; `readSuperblock` validates; `CurrentVersion` bumps +1 with migration strategy for existing test data.
|
||||
4. **Addendum A Addition 1 (test infra only)** — unit tests for the adapter are parameterized over both `walstore` and `smartwal.Store` impls (full scenario matrix in T3c).
|
||||
|
||||
Out of T3a (per T3.0 §12 + Addendum A):
|
||||
- `DurableProvider` → T3b
|
||||
- Recovery integration → T3b
|
||||
- Frontend-side `SYNC_CACHE` / Flush wires → T3b
|
||||
- 4 G6 scenarios → T3c
|
||||
- Perf smoke → T3c
|
||||
|
||||
---
|
||||
|
||||
## 1. File layout
|
||||
|
||||
### 1.1 New files
|
||||
|
||||
| Path | Purpose | Est LOC |
|
||||
|---|---|---|
|
||||
| `core/frontend/durable/storage_adapter.go` | `StorageBackend` struct + Read/Write/Sync/Close/Identity/SetOperational + byte↔LBA math + fence check | 350-450 |
|
||||
| `core/frontend/durable/storage_adapter_test.go` | Unit tests (matrix-parameterized over walstore + smartwal) | 350-400 |
|
||||
|
||||
### 1.2 Modified files
|
||||
|
||||
| Path | Change | Est LOC delta |
|
||||
|---|---|---|
|
||||
| `core/frontend/types.go` | Add `Sync(ctx) error` + `SetOperational(ok bool, evidence string)` to `Backend` interface | +4 |
|
||||
| `core/frontend/testback/testback.go` | `RecordingBackend` + `StaleRejectingBackend` gain trivial `Sync` (returns nil) + `SetOperational` (store in atomic.Value for diagnostics) | +20 |
|
||||
| `core/storage/superblock.go` | Add `ImplKind uint8` + `ImplVersion uint32` fields; extend serialization; bump `CurrentVersion` to next value; validate in `readSuperblock` | +30 |
|
||||
| `core/storage/walstore.go` | Pass `ImplKind=1` at `CreateWALStore` | +2 |
|
||||
| `core/storage/smartwal/store.go` | Pass `ImplKind=2` at `CreateStore` | +2 |
|
||||
| `core/storage/superblock_test.go` (if exists) + new `superblock_impl_identity_test.go` | Schema-bump migration test; unknown-ImplKind validation | +80 |
|
||||
| Existing QA test backends (`writeCountingBackend`, `countingFaultBackend` in QA test files) | Gain trivial `Sync` + `SetOperational` | +15 |
|
||||
|
||||
### 1.3 Touched but not modified
|
||||
|
||||
- `core/storage/logical_storage.go` — interface unchanged (stop rule §5.5 in audit)
|
||||
- `core/frontend/nvme/*.go` — SYNC wiring deferred to T3b; NVMe Flush still no-op in T3a; behavior unchanged
|
||||
- `core/frontend/iscsi/*.go` — same, SYNC_CACHE wiring in T3b
|
||||
|
||||
### 1.4 Total T3a LOC budget
|
||||
|
||||
| Line class | Budget |
|
||||
|---|---|
|
||||
| Production | 480-630 (per audit §10.8 Addendum A revised estimate) |
|
||||
| Test | 350-400 |
|
||||
| Test-backend shim | ~35 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Acceptance criteria (gate 1 full — QA single-sign per §8C.2)
|
||||
|
||||
| # | Criterion | Evidence |
|
||||
|---|---|---|
|
||||
| 1 | `StorageBackend` implements `frontend.Backend` complete — all 6 methods (Read, Write, Sync, SetOperational, Identity, Close) | Compile + unit test `TestT3a_StorageBackend_ImplementsBackend` |
|
||||
| 2 | Byte↔LBA translation correct across sizes — writes spanning LBA boundaries aggregate, partial-block reads zero-pad tail | `TestT3a_StorageBackend_ByteLBATranslation_Matrix` |
|
||||
| 3 | Per-I/O fence check rejects stale lineage — preserves `INV-FRONTEND-002.EPOCH / .EV / .REPLICA / .HEALTHY` under durable backend | `TestT3a_StorageBackend_FenceCheck_*` (4 sub-tests, one per facet) |
|
||||
| 4 | Operational gate honored — before `SetOperational(true, _)`, every I/O returns `ErrNotReady`; after flip, I/O proceeds | `TestT3a_StorageBackend_OperationalGate_*` |
|
||||
| 5 | `Sync` dispatches to `LogicalStorage.Sync` and returns error verbatim | `TestT3a_StorageBackend_Sync_DispatchesToStorage` |
|
||||
| 6 | Addition 1 matrix: ALL adapter unit tests run against both `walstore` and `smartwal.Store` impls | Table-driven tests with factory slice; every test case executes N×2 |
|
||||
| 7 | Addition 2: superblock on create records `ImplKind`=1 or 2; readSuperblock validates; unknown kind → `ErrInvalidSuperblock` | `TestT3a_Superblock_ImplKind_*` |
|
||||
| 8 | Addition 2: `CurrentVersion` bump is forward-only; existing test data either migrated or regenerated; no silent-corrupt read of old format | `TestT3a_Superblock_VersionBump_*` + regen any existing test fixtures |
|
||||
| 9 | Backend interface extension doesn't break existing tests — all prior T0/T1/T2 unit + component + L2 tests stay green | `go test ./core/... -count=1` |
|
||||
| 10 | Boundary guard green — new `core/frontend/durable/` package does not import `core/authority` or `core/adapter` | `boundary_guard_test.go` extended to cover `core/frontend/durable/` |
|
||||
|
||||
All 10 green → QA single-signs T3a close → T3b opens.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stop rules (T3a-specific + inherited)
|
||||
|
||||
Inherited from T3 sketch §6 + audit §5:
|
||||
- Port-model default; no pragmatic evolution of the 7 V2 muscle files' behavior
|
||||
- Storage NEVER publishes authority; adapter NEVER advances epoch
|
||||
- `writeGate` NEVER reappears in `core/storage/`
|
||||
- `LogicalStorage` interface signature NEVER changes in T3
|
||||
|
||||
T3a-specific:
|
||||
1. **Adapter file is atomic** — G-int.1 + G-int.2 + G-int.3 ship in one commit. No partial adapter that T3b retrofits. If unable to land atomically (e.g., interface extension breaks too many tests to fix in one commit), sw escalates §8C.3 trigger (architectural issue) instead of splitting.
|
||||
2. **Interface extension atomic with consumers** — `Backend` interface additions ship in same commit as all existing `Backend` implementations' trivial impls. No "interface added in commit A, implementations in commit B" — that breaks the build.
|
||||
3. **Superblock schema bump is one-way** — `CurrentVersion++` lands with the `ImplKind` + `ImplVersion` additions; tests with hard-coded old-format bytes regen in same commit. If any existing production data exists on disk (none expected since T3 hasn't shipped), migration is a separate §8C.3 trigger.
|
||||
4. **Matrix test factory is a shared helper** — adapter + superblock tests use one `logicalStorageFactories()` helper returning both walstore + smartwal constructor factories. Duplicating the factory list in multiple test files is a review-reject (makes adding new impl later require N edits).
|
||||
5. **No frontend wiring in T3a** — NVMe Flush + iSCSI SYNC_CACHE handlers stay no-op. Wiring them = §8C.3 trigger #3 (cross-batch pollution).
|
||||
|
||||
---
|
||||
|
||||
## 4. Ledger rows queued (ACTIVE at T3a close)
|
||||
|
||||
| ID | Statement |
|
||||
|---|---|
|
||||
| `INV-DURABLE-OPGATE-001` | Before `SetOperational(true, _)`, all I/O returns `ErrNotReady`; preserves `INV-FRONTEND-002.*` under durable backend |
|
||||
| `INV-DURABLE-IMPL-IDENTITY-001` | Superblock records `ImplKind` + `ImplVersion`; mismatch between stored ImplKind and opener's selector is rejected, not silently coerced |
|
||||
|
||||
Existing `INV-FRONTEND-002.EPOCH / .EV / .REPLICA / .HEALTHY` rows MUST remain ACTIVE post-T3a (adapter preserves them under durable).
|
||||
|
||||
---
|
||||
|
||||
## 5. Sign-off
|
||||
|
||||
### 5.1 QA single-sign (T3a open)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ **SIGNED — T3a open for sw implementation** per §8C.2 |
|
||||
|
||||
Sw may now begin T3a code commit.
|
||||
|
||||
### 5.2 Sw delivery log (fill on commit)
|
||||
|
||||
| Date | Action | Commit |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | T3a code delivered | `0e1595c` |
|
||||
| 2026-04-22 | QA §2 acceptance walk-through complete: 10/10 green (incl. bonus Close-semantics test) | — |
|
||||
| 2026-04-22 | **QA T3a-close single-sign per §8C.2** — T3a CLOSED | — |
|
||||
|
||||
### 5.3 Review + close
|
||||
|
||||
QA runs §2 acceptance 10-row walk-through after sw commit. All 10 green → QA single-signs T3a close → T3b mini-plan opens.
|
||||
|
||||
If any § 2 criterion fails:
|
||||
- Minor (test wiring / layout): sw + QA iterate mid-batch (QA-autonomous per §8C.2)
|
||||
- Major (architectural mismatch in adapter or superblock schema): §8C.3 trigger — escalate architect + PM
|
||||
|
||||
---
|
||||
|
||||
## 6. Change log
|
||||
|
||||
| Date | Change | Author |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | Initial T3a mini plan; QA single-signed; sw cleared to code | QA Owner |
|
||||
@@ -0,0 +1,178 @@
|
||||
# V3 Phase 15 T3b — Mini Port Plan
|
||||
|
||||
**Date**: 2026-04-22
|
||||
**Status**: QA single-signed per §8C.2; sw cleared to start
|
||||
**Owner**: QA drafts + signs; sw implements
|
||||
**Predecessor**: T3a CLOSED (commit `0e1595c`; mini-plan `v3-phase-15-t3a-mini-plan.md`)
|
||||
**Gate**: T3b — Provider + Recovery + frontend Sync wiring
|
||||
**Successor**: T3c mini-plan (scenarios + continuity smoke + perf baseline + T3-end three-sign)
|
||||
**Governing methodology**: §8B PCDD + §8C accelerated-cadence governance
|
||||
|
||||
---
|
||||
|
||||
## 0. Scope (locked)
|
||||
|
||||
T3b delivers in one commit (atomic; splitting is a §8C.3 trigger):
|
||||
|
||||
1. **G-int.4 `DurableProvider`** — new `core/frontend/durable/provider.go` implementing `frontend.Provider`:
|
||||
- `Open(ctx, volumeID)` selects impl (`smartwal` or `walstore`) from config; opens the underlying `LogicalStorage` instance; wraps in T3a `StorageBackend`; returns
|
||||
- Enforces Addendum A #2: reads superblock, compares `ImplKind` vs selector; mismatch → fail-fast with named error (NOT silent coerce)
|
||||
- Production default: `smartwal` (per PM direction "prod 如果能用 smart 最好"); `walstore` remains reachable via config
|
||||
- `testback.StaticProvider` unchanged (per audit §11 G-int.4: tests keep injection path)
|
||||
|
||||
2. **G-int.5 Recovery integration** — new `core/frontend/durable/recovery.go`:
|
||||
- `Recover(storage LogicalStorage) (RecoveryReport, error)` — wraps `LogicalStorage.Recover()` + surfaces recovered LSN + any lineage-drift evidence
|
||||
- Wired from `cmd/blockvolume` startup via a new thin recovery coordinator
|
||||
- After `Recover` success: adapter's `SetOperational(true, evidence)` called → volume host's `HealthyPathExecutor.Probe` proceeds (T1 path, no new publication surface)
|
||||
- If `Recover` returns error OR recovered state disagrees with assigned lineage (per-I/O fence check will catch it anyway): `SetOperational(false, evidence)` + report `NotReady` via existing `status_server.go` `/status` (read-side only, no publication)
|
||||
- **Option 3 from audit §10.5 honored**: no local epoch mirror added; per-I/O adapter fence check remains sole authority-drift guard
|
||||
|
||||
3. **iSCSI `SYNCHRONIZE_CACHE` wire** — modify `core/frontend/iscsi/scsi.go`:
|
||||
- `SYNCHRONIZE_CACHE(10)` (0x35) and `SYNCHRONIZE_CACHE(16)` (0x91) handlers currently no-op; wire to `backend.Sync(ctx)`
|
||||
- Error from `Sync` maps to spec-legal SCSI status (sw picks; `GOOD` on success, `MEDIUM_ERROR` / `HARDWARE_ERROR` on fault per SPC-5)
|
||||
|
||||
4. **NVMe `Flush` wire** — modify `core/frontend/nvme/io.go`:
|
||||
- `ioFlush` opcode (0x00) handler currently no-op; wire to `backend.Sync(ctx)`
|
||||
- Error from `Sync` maps to spec-legal NVMe status (sw picks; generic success 0x00 / internal-error on fault per NVMe 1.4)
|
||||
|
||||
5. **Adapter lifecycle — sw design note honored**:
|
||||
- Per T3a sw close note: `Backend.Close()` does NOT tear down underlying `LogicalStorage`; Provider retains storage handle and closes it explicitly
|
||||
- Double-close of adapter is idempotent
|
||||
- Provider-level shutdown (`cmd/blockvolume` SIGTERM) closes both adapter and underlying storage in correct order
|
||||
|
||||
Out of T3b (per T3 sketch + audit §12):
|
||||
- 4 G6 scenarios → T3c
|
||||
- m01 iSCSI + NVMe fs-workload smoke against durable backend → T3c
|
||||
- Perf smoke + baseline doc → T3c
|
||||
- T3 closure report + T3-end three-sign → T3c
|
||||
- Any reinstatement of `OnDegraded` on `GroupCommitterConfig` — confirmed NOT needed per T3.0 sw note A; storage stays pure-mechanism; degraded-signal lives at adapter/host layer in T3b Recovery
|
||||
|
||||
---
|
||||
|
||||
## 1. File layout
|
||||
|
||||
### 1.1 New files
|
||||
|
||||
| Path | Purpose | Est LOC |
|
||||
|---|---|---|
|
||||
| `core/frontend/durable/provider.go` | `DurableProvider` + config + impl selector + ImplKind mismatch detection | 200-250 |
|
||||
| `core/frontend/durable/provider_test.go` | Unit tests (matrix over both impls; mismatch negative cases) | 200-250 |
|
||||
| `core/frontend/durable/recovery.go` | `Recover` wrapper + `RecoveryReport` + evidence string builder | 100-150 |
|
||||
| `core/frontend/durable/recovery_test.go` | Unit tests (clean recover / mismatch / storage error path) | 150-200 |
|
||||
|
||||
### 1.2 Modified files
|
||||
|
||||
| Path | Change | Est LOC delta |
|
||||
|---|---|---|
|
||||
| `core/frontend/iscsi/scsi.go` | Wire `SYNCHRONIZE_CACHE(10)` + `SYNCHRONIZE_CACHE(16)` handlers to `backend.Sync(ctx)`; map error to SCSI status | +30 |
|
||||
| `core/frontend/nvme/io.go` | Wire `ioFlush` opcode handler to `backend.Sync(ctx)`; map error to NVMe status | +20 |
|
||||
| `cmd/blockvolume/main.go` (or equivalent setup site) | Wire `DurableProvider` into volume host + invoke `Recover` at startup + flip adapter operational bit | +40-60 |
|
||||
| `core/frontend/iscsi/scsi_test.go` + existing iSCSI tests | Add test for SYNC_CACHE handler wiring (one test asserts `backend.Sync` called) | +30 |
|
||||
| `core/frontend/nvme/io_test.go` + existing NVMe tests | Add test for Flush opcode wiring | +20 |
|
||||
| `core/host/volume/healthy_executor*` (if needed) | Only if Recover-to-Healthy wiring requires a new hook; otherwise untouched | +0-20 |
|
||||
|
||||
### 1.3 Touched but not modified
|
||||
|
||||
- `core/frontend/durable/storage_adapter.go` — T3a delivered; T3b consumes but doesn't change
|
||||
- `core/frontend/types.go` — interface stable post-T3a
|
||||
- `core/storage/*` — `LogicalStorage` stable; no schema or behavior change in T3b
|
||||
- `core/frontend/testback/*` — unchanged; kept for tests per audit §11 G-int.4
|
||||
|
||||
### 1.4 Total T3b LOC budget
|
||||
|
||||
| Line class | Budget |
|
||||
|---|---|
|
||||
| Production | 350-450 (provider + recovery + 2 frontend wires + cmd wire) |
|
||||
| Test | 250-300 (provider + recovery + frontend handler tests) |
|
||||
| Total | 600-750 |
|
||||
|
||||
Wall-clock: 1-1.5 day (per audit §12).
|
||||
|
||||
---
|
||||
|
||||
## 2. Acceptance criteria (gate — QA single-sign per §8C.2)
|
||||
|
||||
| # | Criterion | Evidence |
|
||||
|---|---|---|
|
||||
| 1 | `DurableProvider` implements `frontend.Provider` | Compile + `TestT3b_DurableProvider_ImplementsProvider` |
|
||||
| 2 | `Provider.Open` selects impl by config (`smartwal` default in prod; `walstore` reachable) | `TestT3b_DurableProvider_Open_SelectsImpl_Matrix` (both impls) |
|
||||
| 3 | Returned Backend starts `operational=false` until Recovery flips it | `TestT3b_DurableProvider_Open_StartsNotOperational` |
|
||||
| 4 | Addendum A #2: ImplKind mismatch between selector and on-disk superblock → named error (not silent coerce) | `TestT3b_DurableProvider_Open_ImplKindMismatch_FailsFast` |
|
||||
| 5 | `Recover(storage)` calls `LogicalStorage.Recover()`; on success returns RecoveryReport with `recoveredLSN`; on error returns wrapped error + NotReady evidence | `TestT3b_Recovery_CleanRecover_*` + `TestT3b_Recovery_StorageErrorPath_*` |
|
||||
| 6 | iSCSI `SYNCHRONIZE_CACHE(10)` + `SYNCHRONIZE_CACHE(16)` handlers dispatch to `backend.Sync(ctx)` | `TestT3b_ISCSI_SyncCache_DispatchesToBackend` (both opcodes) |
|
||||
| 7 | NVMe `ioFlush` opcode handler dispatches to `backend.Sync(ctx)` | `TestT3b_NVMe_Flush_DispatchesToBackend` |
|
||||
| 8 | Backend Close does NOT tear down underlying storage; Provider.Close() tears down in correct order; double-close idempotent | `TestT3b_DurableProvider_Lifecycle_*` |
|
||||
| 9 | Full regression including T3a + prior T0/T1/T2 suite green | `go test ./core/... -count=1` → all packages ok |
|
||||
| 10 | Boundary guard green — `core/frontend/durable/` + `cmd/blockvolume` wire path still no `core/authority` / `core/adapter` imports from frontend | `TestFrontendCannotMintAuthority_BoundaryGuard` (existing recursive walk) |
|
||||
|
||||
All 10 green → QA single-signs T3b close → T3c mini-plan opens.
|
||||
|
||||
---
|
||||
|
||||
## 3. Stop rules (T3b-specific + inherited)
|
||||
|
||||
Inherited from T3 sketch §6 + audit §5 + T3a §3:
|
||||
- Port-model default for anything ported from V2
|
||||
- Storage NEVER publishes authority; adapter NEVER advances epoch
|
||||
- `writeGate` NEVER reappears
|
||||
- `LogicalStorage` interface signature NEVER changes
|
||||
- `Backend` interface signature NEVER changes in T3b (T3a locked)
|
||||
|
||||
T3b-specific:
|
||||
|
||||
1. **Atomic commit** — all 5 items (Provider + Recovery + iSCSI wire + NVMe wire + cmd wire) ship in one commit. Splitting = §8C.3 trigger.
|
||||
2. **No `OnDegraded` callback on `GroupCommitterConfig`** — confirmed NOT needed per T3.0 sw note A. If T3b implementation finds a case where it's needed, that's a §8C.3 trigger #1 (architectural issue — escalate).
|
||||
3. **No local epoch mirror in superblock** — Option 3 from audit §10.5 locked. Adding a superblock `Epoch` field in T3b is a §8C.3 trigger #4 (port-model principle violation) and requires new Discovery Bridge.
|
||||
4. **Recovery never publishes** — Recovery exposes readiness via `status_server.go` `/status` (read-side). Any new RPC / publication path in Recovery flow = §8C.3 trigger #1.
|
||||
5. **Provider selector is config-driven, not code-branch** — production default is `smartwal`, but the selector is a single config field (string or enum), NOT a compile-time branch or hard-coded path. Enables Addendum A #1 matrix testing in T3c without rebuild.
|
||||
6. **Frontend Sync wire is error-faithful** — iSCSI SYNC_CACHE / NVMe Flush handlers MUST propagate `Sync` errors to spec-legal status. Swallowing errors (returning GOOD regardless) = §8C.3 trigger #1 (data integrity hazard).
|
||||
7. **No scenarios in T3b** — 4 G6 scenarios + continuity smoke + perf baseline stay T3c scope. Any scenario YAML or Go replay landing in T3b = §8C.3 trigger #3 (cross-batch pollution).
|
||||
|
||||
---
|
||||
|
||||
## 4. Ledger rows queued (ACTIVE at T3b close)
|
||||
|
||||
| ID | Statement |
|
||||
|---|---|
|
||||
| `INV-DURABLE-PROVIDER-SELECT-001` | `DurableProvider.Open` selects correct impl per config + fails fast on ImplKind mismatch with on-disk superblock |
|
||||
| `INV-DURABLE-RECOVERY-READSIDE-001` | Recovery exposes readiness via adapter's `SetOperational` + existing `/status` HTTP surface; no new publication path; master authority remains sole Epoch/assignment publisher |
|
||||
| `INV-DURABLE-SYNC-WIRED-001` | iSCSI `SYNCHRONIZE_CACHE(10/16)` + NVMe `Flush` reach `LogicalStorage.Sync`; error propagates to spec-legal host status |
|
||||
|
||||
Existing rows MUST remain ACTIVE post-T3b:
|
||||
- `INV-DURABLE-OPGATE-001` (T3a)
|
||||
- `INV-DURABLE-IMPL-IDENTITY-001` (T3a)
|
||||
- `INV-FRONTEND-002.EPOCH / .EV / .REPLICA / .HEALTHY` (T1)
|
||||
|
||||
---
|
||||
|
||||
## 5. Sign-off
|
||||
|
||||
### 5.1 QA single-sign (T3b open)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ **SIGNED — T3b open for sw implementation** per §8C.2 |
|
||||
|
||||
### 5.2 Sw delivery log (fill on commit)
|
||||
|
||||
| Date | Action | Commit |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | T3b code delivered | `72d0d40` |
|
||||
| 2026-04-22 | QA §2 acceptance walk-through complete: 10/10 green (incl. bonus Open_Caches test) + spot-checks confirmed: SYNC errors map to ASCWriteError (no swallow), Flush errors propagate via `backend.Sync`, zero publication paths in cmd/blockvolume wire, memback fallback preserved when `--durable-root` empty | — |
|
||||
| 2026-04-22 | **QA T3b-close single-sign per §8C.2** — T3b CLOSED | — |
|
||||
| 2026-04-22 (addendum) | **QA L1 addendum landed** — 3 tests in `core/frontend/durable/t3b_qa_l1_addendum_test.go` (V2-reference-driven coverage: ConcurrentIO_SyncBarrier + RecoverThenServe + LargeSpanningWrite). Matrix 6 subtests / 2 impls each PASS. Closes V2-parity gaps at L1 before T3c L2 scenarios. Zero impact on prior T3b close; §8C.2 QA single-sign holds | QA Owner |
|
||||
|
||||
### 5.3 Review + close
|
||||
|
||||
QA runs §2 acceptance 10-row walk-through after sw commit. All 10 green → QA single-signs T3b close → T3c mini-plan opens (scenarios + smoke + perf + closure + T3-end three-sign).
|
||||
|
||||
Minor fails: mid-batch iterate per §8C.2.
|
||||
Major fails (architectural): §8C.3 trigger — escalate architect + PM.
|
||||
|
||||
---
|
||||
|
||||
## 6. Change log
|
||||
|
||||
| Date | Change | Author |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | Initial T3b mini plan; QA single-signed; sw cleared to code | QA Owner |
|
||||
@@ -0,0 +1,151 @@
|
||||
# V3 Phase 15 T3c — Mini Port Plan (LOCKED)
|
||||
|
||||
**Date**: 2026-04-22
|
||||
**Status**: QA single-signed per §8C.2; sw cleared to start
|
||||
**Owner**: QA drafts + signs; sw implements
|
||||
**Predecessor**: T3b CLOSED (commit `72d0d40`)
|
||||
**Successor**: T3 CLOSED → Gate G4 pass → P15 advances
|
||||
**Skeleton→LOCK note**: post-T3b scope review found no shift needed. `--durable-root` + selector shape lets scenarios parameterize via existing `logicalStorageFactories()` helper without new infra. Original §0-§6 body retained below.
|
||||
|
||||
---
|
||||
|
||||
## 0. (Original skeleton preamble)
|
||||
Owner
|
||||
**Purpose**: Preliminary scope frame so sw sees what T3c will look like while working T3b. Do NOT start T3c work from this doc. Final LOCKED mini-plan replaces this file after T3b close.
|
||||
|
||||
---
|
||||
|
||||
## 0. Why a skeleton
|
||||
|
||||
§8C mid-T autonomy means QA can prep next-batch framing while sw works current. This doc captures the **shape** only:
|
||||
- Files to expect
|
||||
- Acceptance criteria headings (body TBD)
|
||||
- Ledger row IDs to queue
|
||||
- Stop rules (all inherited)
|
||||
|
||||
Skeleton DOES NOT:
|
||||
- Commit file-level LOC budgets
|
||||
- Lock test names
|
||||
- Sign-off (QA does NOT sign a skeleton; sign lives on the real mini-plan post T3b-close)
|
||||
|
||||
If T3b delivery uncovers scope shift (e.g., a Provider detail that changes T3c scenario shape), QA edits this skeleton before locking.
|
||||
|
||||
---
|
||||
|
||||
## 1. T3c scope frame
|
||||
|
||||
T3c is the closing batch — scenarios + continuity + perf + T3-end three-sign.
|
||||
|
||||
### 1.1 Scenarios (6 YAMLs + Go replay, expanded per V2-parity design 2026-04-22)
|
||||
|
||||
**Original 4 (from audit §11 G-int.6 + §3.3)** + **2 additional (V2-parity additions for crash variant coverage)**.
|
||||
|
||||
| ID | Scenario | Variant matrix | Purpose |
|
||||
|---|---|---|---|
|
||||
| `t3c-durable-crash-recovery` | Write → ack → SIGKILL → restart → Read byte-exact | walstore × smartwal | **Canonical G4 pass gate** |
|
||||
| `t3c-durable-fsync-boundary` | Unflushed Write → kill → restart → Read pre-or-post bytes, never corrupt | walstore × smartwal | fsync semantics |
|
||||
| `t3c-durable-disk-fill` | Fill backing store → Write → graceful error | walstore × smartwal | fail-hard |
|
||||
| `t3c-durable-wal-replay` | N acked writes + crash → N recoverable | walstore × smartwal | WAL replay correctness |
|
||||
| ~~`t3c-durable-crash-during-sync`~~ (**DEFERRED** to inventory) | ~~Sync atomicity boundary; V2 parity (CP13)~~ | — | Post-sw-commit addition; shipped 4-scenario set covers canonical G4 + adjacent; inventoried as B-tier |
|
||||
| ~~`t3c-durable-restart-loop`~~ (**DEFERRED** to inventory) | ~~Multi-recover drift; V2 parity (t0-hosting-smoke)~~ | — | Same — inventoried as B-tier post-T3-closed follow-up |
|
||||
|
||||
Each scenario lands as:
|
||||
- YAML spec in `testrunner/scenarios/testdata/` (shape documentation)
|
||||
- Go replay in `core/frontend/durable/scenario_*_test.go` — matrix-parameterized over both impls via `logicalStorageFactories()` (shared helper from T3a)
|
||||
|
||||
### 1.2 Frontend continuity smoke
|
||||
|
||||
Existing T2 iSCSI + NVMe smoke tests re-run with `DurableProvider` replacing `testback.StaticProvider` wiring in `cmd/blockvolume` test harness. No new tests — regression against durable backend.
|
||||
|
||||
### 1.3 Perf baseline (characterization only, NO threshold)
|
||||
|
||||
- Single fio workload: 4 KiB random write, QD=8, 60s
|
||||
- Run against both walstore + smartwal (matrix per Addendum A #1)
|
||||
- Output: `testrunner/perf/t3c-durable-baseline.md` with throughput + p50/p99 latency
|
||||
- NO pass/fail gate; just published numbers per audit §11 G-int.6 and T3 sketch §3.5
|
||||
|
||||
### 1.4 Closure report for T3
|
||||
|
||||
New doc: `v3-phase-15-t3-closure-report.md`. Structure mirrors `v3-phase-15-t2-closure-report-draft.md`:
|
||||
- §A batch history (T3.0 / T3a / T3b / T3c commits + status)
|
||||
- §B delivery summary + non-claims + governance transition
|
||||
- §C V2 port depth audit (7 muscle files — MATCHES / MATCHES-BETTER / GAP verdicts from T3.0 §10.3)
|
||||
- §D new ledger rows (6 `INV-DURABLE-*` + supporting)
|
||||
- §E T3-end three-sign signatures (architect + PM + QA)
|
||||
|
||||
Plus: update `v3-invariant-ledger.md` change log with T3c closure batch row.
|
||||
|
||||
---
|
||||
|
||||
## 2. Acceptance criteria (headings — bodies filled post T3b-close)
|
||||
|
||||
~10 criteria expected:
|
||||
1. All 4 scenarios pass on both impls (walstore + smartwal)
|
||||
2. Frontend continuity: T2 iSCSI smoke + T2 NVMe smoke pass against `DurableProvider`
|
||||
3. m01 fs-workload smoke — `iterate-m01-nvme.sh` + equivalent iSCSI run green against durable backend (wall-clock bound: 10 cycles each, not 24h soak)
|
||||
4. Perf baseline artifact filed; matrix run both impls
|
||||
5. Full regression including T3a+T3b+T3c suite green
|
||||
6. Boundary guard clean
|
||||
7. All 6 new T3 ledger rows ACTIVE: `INV-DURABLE-001 / -WAL-REPLAY-001 / -FSYNC-BOUNDARY-001 / PCDD-DURABLE-DISK-FULL-001 / -IMPL-IDENTITY-001 (T3a) / -OPGATE-001 (T3a) / -PROVIDER-SELECT-001 (T3b) / -RECOVERY-READSIDE-001 (T3b) / -SYNC-WIRED-001 (T3b)`
|
||||
8. T2's 16 existing ACTIVE rows still ACTIVE
|
||||
9. Closure report `v3-phase-15-t3-closure-report.md` filled end-to-end
|
||||
10. **T3-end three-sign** (architect + PM + QA per §8C.1)
|
||||
|
||||
---
|
||||
|
||||
## 3. Stop rules (all inherited — no T3c-specific additions expected)
|
||||
|
||||
- Port-model default (T3 sketch §6 + audit §5 + T3a §3 + T3b §3)
|
||||
- No scope creep into T4 (replication, failover, rebuild) — §8C.3 trigger #3
|
||||
- No new production surface beyond T3a+T3b deliverables (perf harness + scenario replays are test infra only)
|
||||
|
||||
---
|
||||
|
||||
## 4. Ledger row plan (queued ACTIVE at T3c close)
|
||||
|
||||
| ID | Where tested |
|
||||
|---|---|
|
||||
| `INV-DURABLE-001` | Scenario `t3c-durable-crash-recovery` |
|
||||
| `INV-DURABLE-WAL-REPLAY-001` | Scenario `t3c-durable-wal-replay` |
|
||||
| `INV-DURABLE-FSYNC-BOUNDARY-001` | Scenario `t3c-durable-fsync-boundary` |
|
||||
| `PCDD-DURABLE-DISK-FULL-001` | Scenario `t3c-durable-disk-fill` |
|
||||
|
||||
Plus continuity validation: T2 frontend rows (NVMe + iSCSI) proven to survive backend swap from memback to durable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sign-off
|
||||
|
||||
### 5.1 QA single-sign (T3c open)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ **SIGNED — T3c OPENED** per §8C.2 |
|
||||
|
||||
### 5.1.1 T3c CLOSE (§8C.2 single-sign)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| QA Owner | Claude (QA agent) | 2026-04-22 | ✅ **T3c CLOSED**. Sw delivered commit `829c6a9` (4 scenarios × 2 impls + perf first-light + 4 YAML shape docs) + `5c33460` (T3b follow-up integration matrix) meeting mini-plan §2 acceptance 7/7 sw-side. QA completed #3 m01 fs-workload 4-matrix green + #9 closure report review. 2 scenarios (crash-during-sync + restart-loop) deferred to inventory per §1.1 revision. Ready for T3-end three-sign. |
|
||||
|
||||
Sw may now begin T3c code commit.
|
||||
|
||||
### 5.2 T3c-end three-sign (post-T3c delivery)
|
||||
|
||||
| Role | Signer | Date | Decision |
|
||||
|---|---|---|---|
|
||||
| Architect | _________ | _________ | ⏸ |
|
||||
| PM | _________ | _________ | ⏸ |
|
||||
| QA Owner | _________ | _________ | ⏸ |
|
||||
|
||||
**Effect upon three-sign**: T3 CLOSED, Gate G4 passes, P15 advances to next gate (G5 Replicated Write Path per canonical roadmap OR whichever canonical sequence defines).
|
||||
|
||||
---
|
||||
|
||||
## 6. Change log
|
||||
|
||||
| Date | Change | Author |
|
||||
|---|---|---|
|
||||
| 2026-04-22 | Initial skeleton drafted while sw works T3b; detail LOCKED post-T3b-close | QA Owner |
|
||||
| 2026-04-22 | Post-T3b LOCK + QA single-sign | QA Owner |
|
||||
| 2026-04-22 | V2-parity scope addition: §1.1 expanded from 4→6 scenarios (`t3c-durable-crash-during-sync` + `t3c-durable-restart-loop`). Rationale: V2 CP13 + t0-hosting-smoke 20-cycle parity. Wall-clock +half-day; zero impact on T3b close. QA L1 addendum (3 tests in `t3b_qa_l1_addendum_test.go`) landed separately in T3b. L3 `iterate-m01-nvme.sh` extended with Matrix C (1000 small files, group_commit batching) + Matrix D (5 remount cycles, cross-session consistency). | QA Owner |
|
||||
Reference in New Issue
Block a user