kv(hlc): observe applied commit_ts into HLC.last — HLC-4 strategy (c)#859
Conversation
Implements the M1 spec follow-up per
docs/design/2026_05_28_partial_tla_safety_spec.md §5.1 HLC-4
precondition (ii), strategy (c) — "scan the FSM for max committed
HLC and Observe before serving any persistence Next()".
Refinement over the spec doc's strategy (c) description: instead of
detecting a new term in ShardedCoordinator and calling Observe once
at election time, the FSM observes every commit_ts on apply. This
is semantically equivalent for HLC-4 (etcd/raft applies all
uncommitted prior-term entries before the new leader serves a write,
so by the time HLC.Next() runs in the new term, hlc.last >= every
prior commit), and has two advantages over the on-election form:
1. No protocol change to ShardedCoordinator or RunHLCLeaseRenewal.
2. No race window between BecomeLeader and the first Observe — a
concurrent client request landing in that window cannot see a
stale hlc.last.
Concretely, one line in kv/fsm.go applyRequestErr after a successful
handleRequest:
if f.hlc != nil && commitTS > 0 {
f.hlc.Observe(commitTS)
}
f.hlc.Observe is the existing CAS-to-max method
(kv/hlc.go:129-140), so this is a pure addition with no signature
change. Apply is the only call site exercised; no Next() caller
audit required.
Tests (kv/fsm_hlc_observe_test.go):
- TestApplyObservesCommitTSIntoHLC — verifies Apply advances hlc.last
via Observe(commitTS), preserves monotonicity on a stale-ts apply.
- TestApplyHLCObserveAfterRestart — simulates the spec's
new-leader-after-restart scenario: a fresh HLC (last = 0) re-plays
a prior leader's write through the FSM and the resulting Next()
is strictly greater than the prior commit_ts.
The ceiling-fence half of the M1 spec contract (HLC-4 precondition
(iii) — making HLC.Next() fail-closed when wall_now >=
physicalCeiling) is intentionally deferred to a follow-up PR. That
change alters HLC.Next()'s return signature from `uint64` to
`(uint64, error)` and requires auditing 18 production call sites
across kv/, adapter/, and store/ (per the spec doc and the loop-
discipline reminder for semantic changes); splitting it from this
strategy-(c) PR keeps each diff individually reviewable.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
More reviews will be available in 46 minutes and 19 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
TLA+ spec divergence review (auto-triggered)This PR touches files that the TLA+ safety spec has an anchor on (per Anchored files changed in this PR head (dafd215):
What to check, by subsystem:
If the change is correct but requires a spec update, edit @claude review please verify TLA+ spec divergence per the checklist above. @codex review please verify TLA+ spec divergence per the checklist above. |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Code Review
This pull request implements the HLC-4 strategy (c) in the FSM by observing every applied commit timestamp to ensure the node's Hybrid Logical Clock (HLC) dominates the maximum committed timestamp visible in the FSM. This prevents logical-handoff gaps when a follower is elected as a leader. It also introduces comprehensive unit tests (TestApplyObservesCommitTSIntoHLC and TestApplyHLCObserveAfterRestart) to verify this behavior under normal operation and post-restart recovery scenarios. There are no review comments, so I have no feedback to provide.
Summary
Implements the M1 spec follow-up per
docs/design/2026_05_28_partial_tla_safety_spec.md§5.1 HLC-4 precondition (ii) — strategy (c) "scan the FSM for max committed HLC andObservebefore serving any persistenceNext()".Follows PR #856 (M1 TLA+ spec).
What lands
One line in
kv/fsm.goapplyRequestErrafter a successfulhandleRequest:HLC.Observeis the existing CAS-to-max method (kv/hlc.go:129-140), so this is a pure addition with no signature change and no caller audit required.Why "observe on every apply" vs the spec doc's "observe on election"
The spec doc describes strategy (c) as "on election, scan the FSM and call
Observe". The implementation refines this to "Observe on every apply". The two are semantically equivalent for HLC-4 because etcd/raft applies all uncommitted prior-term entries before a new leader serves a write — so by the timeHLC.Next()runs in the new term,hlc.last >= every prior commit.Two advantages over the on-election form:
ShardedCoordinatororRunHLCLeaseRenewal.BecomeLeaderand the firstObserve— a concurrent client request landing in that window cannot see a stalehlc.last.Tests
kv/fsm_hlc_observe_test.go:TestApplyObservesCommitTSIntoHLC— verifies Apply advanceshlc.lastviaObserve(commitTS), and preserves monotonicity on a stale-ts apply.TestApplyHLCObserveAfterRestart— simulates the spec's new-leader-after-restart scenario: a fresh HLC (last = 0) re-plays a prior leader's write through the FSM and the resultingNext()is strictly greater than the priorcommit_ts.Both tests pass with
-race.Deferred to a follow-up PR
The ceiling-fence half of the M1 spec contract (HLC-4 precondition (iii) — making
HLC.Next()fail-closed whenwall_now >= physicalCeiling) is intentionally NOT in this PR. That change altersHLC.Next()'s return signature fromuint64to(uint64, error)and requires auditing 18 production call sites acrosskv/,adapter/, andstore/. Splitting it from the strategy-(c) PR keeps each diff individually reviewable per the loop-discipline reminder for semantic changes.Self-review (5-lens per CLAUDE.md)
Observeonly advances an in-memory clock counter, no persistence path touched.Observeis the existing CAS-to-max loop; safe under concurrentApply(which is serial per Raft FSM contract anyway) and concurrentNext(the CAS handles it).ts > currentso steady-state is one load.Observeis monotone (only advances). No semantic change to any other code path.kv/fsm_hlc_observe_test.gocovering the basic apply→observe path and the post-restart catchup case. Existing kv + store tests pass.Test plan
tla-checkworkflow does NOT fire — this PR is Go-only)tla-spec-ai-reviewworkflow (PR ci(tla): auto-request Claude + Codex review on anchored-file changes #857) auto-pings Claude + Codex for spec-divergence reviewBecomeLeader_HLCintla/hlc/HLC.tlaOut of scope
HLC.Next()fail-closed) — separate follow-up PR.