Skip to content

fix(lease-read): apply leadership-loss filter to LeaseRead paths and add transfer-in-progress sentinel#559

Merged
bootjp merged 2 commits intomainfrom
fix/lease-read-leadership-filter
Apr 20, 2026
Merged

fix(lease-read): apply leadership-loss filter to LeaseRead paths and add transfer-in-progress sentinel#559
bootjp merged 2 commits intomainfrom
fix/lease-read-leadership-filter

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented Apr 20, 2026

Summary

Follow-up to #558 addressing remaining CodeRabbit + gemini feedback.

  • kv/coordinator.go LeaseRead + kv/sharded_coordinator.go groupLeaseRead: mirror the dispatch-path fix — invalidate the lease only when isLeadershipLossError reports a real leadership signal. Previously a LinearizableRead deadline or transient transport error would force the remainder of the lease window onto the slow path, reproducing the same regression the write-path fix in fix(lease-read): only invalidate lease on leadership-loss errors #558 addressed.
  • internal/raftengine/etcd/engine.go: add errLeadershipTransferInProgress marked with raftengine.ErrLeadershipTransferInProgress, and fail Propose fast when BasicStatus().LeadTransferee != 0 so callers see an errors.Is-matchable error instead of hanging on a proposal that etcd/raft silently drops during transfer.
  • Refresh the stale comment on refreshLeaseAfterDispatch to reflect the filtered invalidation contract.
  • Tests: add the hashicorp ErrLeadershipTransferInProgress marked case to TestIsLeadershipLossError and pin errLeadershipTransferInProgress in the etcd sentinel test.

Test plan

  • go test -race ./kv/... ./internal/raftengine/...
  • golangci-lint clean

…add transfer-in-progress sentinel

Addresses CodeRabbit and gemini feedback on PR #558.

- kv/coordinator.go LeaseRead + kv/sharded_coordinator.go groupLeaseRead:
  mirror the dispatch-path fix by invalidating the lease only when
  isLeadershipLossError reports a real leadership signal. Previously a
  LinearizableRead deadline or transient transport error would force the
  remainder of the lease window onto the slow path, reproducing the same
  regression the write-path fix addressed.
- internal/raftengine/etcd/engine.go: add errLeadershipTransferInProgress
  marked with raftengine.ErrLeadershipTransferInProgress, and fail Propose
  fast when BasicStatus().LeadTransferee != 0 so callers see an errors.Is-
  matchable error instead of hanging on a proposal etcd/raft silently
  drops during transfer.
- Refresh the stale comment on refreshLeaseAfterDispatch to reflect the
  filtered invalidation contract.
- Tests: add the hashicorp ErrLeadershipTransferInProgress marked case
  to kv.TestIsLeadershipLossError and pin errLeadershipTransferInProgress
  in the etcd sentinel test.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 20, 2026

/gemini review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Warning

Rate limit exceeded

@bootjp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 40 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7c125f0-a1c7-4590-8b51-d3d33f0a5db6

📥 Commits

Reviewing files that changed from the base of the PR and between c67b6a0 and f93ae7c.

📒 Files selected for processing (5)
  • internal/raftengine/etcd/engine.go
  • internal/raftengine/etcd/engine_test.go
  • kv/coordinator.go
  • kv/lease_state_test.go
  • kv/sharded_coordinator.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/lease-read-leadership-filter

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the errLeadershipTransferInProgress error and a fast-fail check in the etcd raft engine to prevent proposal hangs during leadership transfers. It also updates KV coordinators to only invalidate leases on actual leadership loss, avoiding performance degradation from transient errors. Review feedback identifies that ReadIndex and configuration change paths are also susceptible to similar hangs and should include the same check for consistency.

Comment on lines +1035 to +1038
if e.rawNode.BasicStatus().LeadTransferee != 0 {
req.done <- proposalResult{err: errors.WithStack(errLeadershipTransferInProgress)}
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While this fast-fail check correctly prevents proposals from hanging during a leadership transfer, the same issue affects ReadIndex requests in handleRead. In etcd/raft, MsgReadIndex is also silently dropped when leadTransferee != 0, which will cause LinearizableRead (and consequently LeaseRead) to hang until the context deadline.

Since this PR specifically aims to improve the LeaseRead path, and LeaseRead relies on LinearizableRead (which calls handleRead), missing this check in handleRead (around line 1051) will leave the read path susceptible to hangs during transfers. Consider applying a similar check to handleRead and the configuration change paths (handleAddVoter, handleRemoveServer) for consistency. When modifying these, ensure that handleAddVoter synchronously publishes the configuration snapshot before advertising membership, and handleRemoveServer triggers a graceful shutdown if the node removes itself, as per repository standards.

References
  1. When adding a new voter to a raft cluster, synchronously publish the configuration snapshot before advertising the new membership to prevent the voter from receiving a stale snapshot.
  2. When a raft node processes a committed configuration change that removes itself from the cluster, it should trigger a graceful shutdown of its own raft engine.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new error sentinel, ErrLeadershipTransferInProgress, to explicitly signal when etcd/raft drops proposals during a leadership transfer. It also refines lease invalidation logic in the KV coordinators to only trigger on genuine leadership loss signals, preventing transient errors from forcing subsequent reads onto the slow path. Feedback suggests extending the leadership transfer check to other Raft entry points for consistency and correcting the documentation regarding proposal drops.

Comment thread internal/raftengine/etcd/engine.go Outdated
Comment on lines +1030 to +1038
// etcd/raft silently drops proposals while a leadership transfer is
// in flight (LeadTransferee != 0). Surface this as a distinct error
// so callers (lease-read invalidation, proxy retry, etc.) can
// recognise it via errors.Is(err, raftengine.ErrLeadershipTransferInProgress)
// instead of hanging on an ack that will never come.
if e.rawNode.BasicStatus().LeadTransferee != 0 {
req.done <- proposalResult{err: errors.WithStack(errLeadershipTransferInProgress)}
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check for LeadTransferee != 0 is a valuable addition for error mapping. To ensure consistency and prevent race conditions (as per repository guidelines for leadership-dependent operations), consider the following:

  1. Consistency: Apply this check to all entry points interacting with the Raft loop (handleRead, handleAddVoter, handleRemoveServer) after any necessary locks are acquired. This ensures consistent leadership-loss signaling and prevents operations from proceeding during a leadership transition.

  2. Comment Accuracy: The comment mentions "hanging on an ack that will never come". However, etcd/raft's RawNode.Propose returns ErrProposalDropped when a transfer is in progress, which is already handled at line 1042. The primary benefit here is the explicit mapping to the shared sentinel.

Suggested change
// etcd/raft silently drops proposals while a leadership transfer is
// in flight (LeadTransferee != 0). Surface this as a distinct error
// so callers (lease-read invalidation, proxy retry, etc.) can
// recognise it via errors.Is(err, raftengine.ErrLeadershipTransferInProgress)
// instead of hanging on an ack that will never come.
if e.rawNode.BasicStatus().LeadTransferee != 0 {
req.done <- proposalResult{err: errors.WithStack(errLeadershipTransferInProgress)}
return
}
// etcd/raft drops proposals while a leadership transfer is in flight
// (LeadTransferee != 0). Surface this as a distinct error so callers
// (lease-read invalidation, proxy retry, etc.) can recognize it via
// errors.Is(err, raftengine.ErrLeadershipTransferInProgress) instead
// of receiving a generic proposal-dropped error.
if e.rawNode.BasicStatus().LeadTransferee != 0 {
req.done <- proposalResult{err: errors.WithStack(errLeadershipTransferInProgress)}
return
}
References
  1. To prevent race conditions in read-modify-write operations that require leadership, verify leadership status after acquiring the lock that protects the operation.

Addresses gemini feedback on PR #559.

etcd/raft silently drops MsgReadIndex while LeadTransferee != 0, and
RawNode.ReadIndex does not return an error on drop. Without a fast-
fail, LinearizableRead/LeaseRead would block on req.done until the ctx
deadline (~read timeout), reintroducing the exact hang the Propose-side
check was meant to prevent but on the read path.

Also refresh the handleProposal comment: etcd/raft returns
ErrProposalDropped (not a hang). The value of the fast-fail is the
explicit mapping to the shared ErrLeadershipTransferInProgress
sentinel, not avoiding a hang.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 20, 2026

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces explicit handling for leadership transfers within the etcd raft engine by implementing the errLeadershipTransferInProgress sentinel error. This allows the engine to fast-fail proposals and read requests during an active transfer, preventing callers from blocking until a deadline. Furthermore, the lease invalidation logic in both the Coordinate and ShardGroup has been updated to only trigger upon confirmed leadership loss signals, preventing transient errors from unnecessarily forcing reads onto the slower linearizable path. I have no feedback to provide as no review comments were present.

@bootjp bootjp merged commit 1a85d7d into main Apr 20, 2026
5 checks passed
@bootjp bootjp deleted the fix/lease-read-leadership-filter branch April 20, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant