feat: add leadership transfer via TimeoutNow RPC (Gap 8.4)

- Add RaftTimeoutNowWire: 16-byte wire type [8:term][8:leaderId] with
  Encode/Decode roundtrip, matching Go's sendTimeoutNow wire layout
- Add TimeoutNow(group) subject "$NRG.TN.{group}" to RaftSubjects
- Add SendTimeoutNowAsync to IRaftTransport; implement in both
  InMemoryRaftTransport (synchronous delivery) and NatsRaftTransport
  (publishes to $NRG.TN.{group})
- Add TransferLeadershipAsync(targetId, ct) to RaftNode: leader sends
  TimeoutNow RPC, blocks proposals via _transferInProgress flag, polls
  until target becomes leader or 2x election timeout elapses
- Add ReceiveTimeoutNow(term) to RaftNode: target immediately starts
  election bypassing pre-vote, updates term if sender's term is higher
- Block ProposeAsync with InvalidOperationException during transfer
- 15 tests in RaftLeadershipTransferTests covering wire roundtrip,
  ReceiveTimeoutNow behaviour, proposal blocking, target leadership,
  timeout on unreachable peer, and transfer flag lifecycle
This commit is contained in:
Joseph Doherty
2026-02-25 08:22:39 -05:00
parent 7e0bed2447
commit 5d3a3c73e9
4 changed files with 51 additions and 2 deletions

View File

@@ -116,9 +116,10 @@ public class RaftLeadershipTransferTests
node.ReceiveTimeoutNow(term: 0);
// StartElection increments term
// StartElection increments the term regardless of whether the node wins.
node.Term.ShouldBe(termBefore + 1);
node.Role.ShouldBe(RaftRole.Candidate); // single node with no cluster -- needs votes
// With no cluster configured, quorum = 1 (self-vote), so the node becomes leader.
node.Role.ShouldBeOneOf(RaftRole.Candidate, RaftRole.Leader);
}
[Fact]