Skip to content

Commit c753c0b

Browse files
auricomclaude
andauthored
fix(rpc): derive /raft/node leadership from raft leader ID (#3266)
* fix(rpc): derive /raft/node leadership from raft leader ID * fix(raft): add LeaderID doc comment; consolidate raft-status tests - Add Go doc comment to Node.LeaderID() describing return value, nil-safety, and staleness semantics, consistent with IsLeader/HasQuorum style. - Consolidate three near-duplicate TestRegisterCustomHTTPEndpoints_RaftNodeStatus* tests into a single table-driven test covering: leaderID==nodeID (is_leader true), leaderID!=nodeID (is_leader false), empty leaderID fallback, and non-GET method (405). Clarifies that is_leader is derived from LeaderID(), not the IsLeader() field on testRaftNodeSource. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(rpc): fix gci import grouping in http_test.go Separate third-party imports from evstack/ev-node-prefixed imports per project gci custom-order config. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 83edf6d commit c753c0b

4 files changed

Lines changed: 117 additions & 1 deletion

File tree

pkg/raft/node.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ func (n *Node) NodeID() string {
257257
return n.config.NodeID
258258
}
259259

260+
// LeaderID returns the server ID of the current cluster leader.
261+
// Returns an empty string if the receiver is nil, raft is uninitialized, or no
262+
// leader has been elected yet. The value may be momentarily stale between raft
263+
// leadership changes; callers that need a strong guarantee should cross-check
264+
// with HasQuorum.
265+
func (n *Node) LeaderID() string {
266+
if n == nil || n.raft == nil {
267+
return ""
268+
}
269+
return n.leaderID()
270+
}
271+
260272
func (n *Node) leaderID() string {
261273
_, id := n.raft.LeaderWithID()
262274
return string(id)

pkg/rpc/server/http.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,16 @@ func RegisterCustomHTTPEndpoints(mux *http.ServeMux, s store.Store, pm p2p.P2PRP
139139
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
140140
return
141141
}
142+
leaderID := raftNode.LeaderID()
143+
isLeader := raftNode.IsLeader()
144+
if leaderID != "" {
145+
isLeader = leaderID == raftNode.NodeID()
146+
}
142147
rsp := struct {
143148
IsLeader bool `json:"is_leader"`
144149
NodeID string `json:"node_id"`
145150
}{
146-
IsLeader: raftNode.IsLeader(),
151+
IsLeader: isLeader,
147152
NodeID: raftNode.NodeID(),
148153
}
149154
w.Header().Set("Content-Type", "application/json")

pkg/rpc/server/http_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"encoding/json"
45
"io"
56
"net/http"
67
"net/http/httptest"
@@ -45,6 +46,103 @@ func TestRegisterCustomHTTPEndpoints(t *testing.T) {
4546
mockStore.AssertExpectations(t)
4647
}
4748

49+
type testRaftNodeSource struct {
50+
isLeader bool
51+
leaderID string
52+
nodeID string
53+
}
54+
55+
func (t testRaftNodeSource) IsLeader() bool {
56+
return t.isLeader
57+
}
58+
59+
func (t testRaftNodeSource) LeaderID() string {
60+
return t.leaderID
61+
}
62+
63+
func (t testRaftNodeSource) NodeID() string {
64+
return t.nodeID
65+
}
66+
67+
func TestRegisterCustomHTTPEndpoints_RaftNodeStatus(t *testing.T) {
68+
type bodyShape struct {
69+
IsLeader bool `json:"is_leader"`
70+
NodeID string `json:"node_id"`
71+
}
72+
73+
cases := []struct {
74+
name string
75+
node testRaftNodeSource
76+
method string
77+
wantStatus int
78+
wantIsLeader bool
79+
wantNodeID string
80+
skipBodyDecode bool
81+
}{
82+
{
83+
// leaderID == nodeID: handler derives is_leader=true from LeaderID(),
84+
// regardless of the IsLeader() field on testRaftNodeSource.
85+
name: "leader matches — is_leader true",
86+
node: testRaftNodeSource{leaderID: "node-a", nodeID: "node-a"},
87+
method: http.MethodGet,
88+
wantStatus: http.StatusOK,
89+
wantIsLeader: true,
90+
wantNodeID: "node-a",
91+
},
92+
{
93+
// leaderID != nodeID: handler derives is_leader=false.
94+
name: "leader differs — is_leader false",
95+
node: testRaftNodeSource{leaderID: "node-b", nodeID: "node-a"},
96+
method: http.MethodGet,
97+
wantStatus: http.StatusOK,
98+
wantIsLeader: false,
99+
wantNodeID: "node-a",
100+
},
101+
{
102+
// empty leaderID: fallback — is_leader=false (no elected leader known).
103+
name: "empty leaderID fallback — is_leader false",
104+
node: testRaftNodeSource{leaderID: "", nodeID: "node-a"},
105+
method: http.MethodGet,
106+
wantStatus: http.StatusOK,
107+
wantIsLeader: false,
108+
wantNodeID: "node-a",
109+
},
110+
{
111+
name: "non-GET method — 405",
112+
node: testRaftNodeSource{},
113+
method: http.MethodPost,
114+
wantStatus: http.StatusMethodNotAllowed,
115+
skipBodyDecode: true,
116+
},
117+
}
118+
119+
for _, tc := range cases {
120+
t.Run(tc.name, func(t *testing.T) {
121+
mux := http.NewServeMux()
122+
RegisterCustomHTTPEndpoints(mux, nil, nil, config.DefaultConfig(), nil, zerolog.Nop(), tc.node)
123+
124+
ts := httptest.NewServer(mux)
125+
t.Cleanup(ts.Close)
126+
127+
req, err := http.NewRequest(tc.method, ts.URL+"/raft/node", nil)
128+
require.NoError(t, err)
129+
resp, err := http.DefaultClient.Do(req) //nolint:gosec // test-only request to httptest server
130+
require.NoError(t, err)
131+
t.Cleanup(func() { _ = resp.Body.Close() })
132+
133+
require.Equal(t, tc.wantStatus, resp.StatusCode)
134+
if tc.skipBodyDecode {
135+
return
136+
}
137+
138+
var body bodyShape
139+
require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
140+
assert.Equal(t, tc.wantIsLeader, body.IsLeader)
141+
assert.Equal(t, tc.wantNodeID, body.NodeID)
142+
})
143+
}
144+
}
145+
48146
func TestHealthReady_aggregatorBlockDelay(t *testing.T) {
49147
logger := zerolog.Nop()
50148

pkg/rpc/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ func (p *P2PServer) GetNetInfo(
368368

369369
type RaftNodeSource interface {
370370
IsLeader() bool
371+
LeaderID() string
371372
NodeID() string
372373
}
373374

0 commit comments

Comments
 (0)