◐ Off-By-One · answer catalog

go-raft-read-index-lease-expiry-fencing

1 answer(s)gogo1.26

go-raft-read-index-lease-expiry-fencing

📦 Source in repository (JSON)

Answer

Done. I built and verified a self-contained Go package and wrote the full solution document to ~/solution.md (1049 lines, balanced fences, includes complete sources).

Summary

Root cause: The read path trusts three facts whose scopes must all agree — leadership, the lease, and quorum acks — but validates them only at capture time and over a single configuration. The concrete defects are:

  1. Lease checked only at read start, never re-validated after waitApplied → serves stale data after an in-flight expiry.
  2. Quorum acks counted without a term check → a depose-racing ack manufactures a false quorum and prevents step-down.
  3. Acks not scoped to a unique ReadID → reordered/delayed responses from an earlier round inflate the current round.
  4. Quorum computed over one voter set → joint consensus can be confirmed by a minority of the old set; config changes leave stale leases valid.
  5. Wall-clock leases without a clock-drift margin.

Fix (fencing): Bind every lease and ack to (term, ReadID, Configuration). - ReadIndex captures (term, commit, cfg), tries the lease fast path, then re-validates the lease after applied >= commit; if lapsed, it falls back to a real quorum round. - The round echoes Term+ReadID; only matching, current-term, in-config acks count; any higher term steps the node down immediately. - hasQuorum requires a majority of every config component (joint consensus); SetConfiguration invalidates the lease. - Leases use a monotonic Clock and electionTimeout − maxClockDrift. - The round returns as soon as quorum is reached (bounded latency, no straggler wait).

Verification (~/readindex, 81% coverage, race-clean):

$ go vet ./...
$ go test -count=1 -race ./...
ok  readindex  1.032s

10 adversarial tests cover lease expiry mid-read, term change before ack, reordered/stale acks, joint-consensus both-majorities, config-change lease invalidation, straggler latency, deposed leader, and a mixed reordering fuzz case.

Artifacts: - ~/solution.md — full write-up + embedded sources - ~/readindex/readindex.go — production fix - ~/readindex/readindex_test.go — adversarial suite - ~/readindex/go.mod

Evidence & signatures

# Evidence
- Problem class: go-raft-read-index-lease-expiry-fencing
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-10-11T10:14:31.040Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement a linearizable read-index path for a Raft-like replicated key-value store where leader leases may expire during an in-flight read and terms can change before the quorum acknowledgment arrives. The implementation must fence stale leaders, preserve bounded read latency under message reordering, and pass adversarial schedules with joint-consensus membership changes.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-raft-read-index-lease-expiry-fencing", "provider": "openrouter", "solved_at": "2026-10-11T10:14:31.044Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog