-
Notifications
You must be signed in to change notification settings - Fork 12
Hub status: surface cluster leadership (IsLeader, LeaderAddr) in Status RPC + ctx hub status #96
Description
Summary
The hub stores a Raft Cluster reference on Server (internal/hub/server.go:54) and even uses it during GracefulStop (server.go:58-62). But the cluster's leadership state is never read by the Status RPC, so operators have no way to ask "which node is leader?" or "am I talking to the leader?" through the supported API surface. The data exists; the wire is missing.
This was tracked in TASKS.md as "Fix hub cluster: NewCluster result is discarded; Raft runs but leadership status is never queryable" (added 2026年04月08日). The "discarded" half is no longer accurate — it was fixed at some point and the task entry didn't get updated. This issue covers only the remaining "leadership status never queryable" half.
What's already in place
Server has the cluster reference and shuts it down properly:
// internal/hub/server.go:48-62 func (s *Server) SetCluster(cluster *Cluster) { s.cluster = cluster } func (s *Server) GracefulStop() { if s.cluster != nil { _ = s.cluster.Shutdown() } s.grpc.GracefulStop() }
The methods to query leadership exist on *Cluster:
// internal/hub/cluster.go:127-141 func (c *Cluster) IsLeader() bool { ... } func (c *Cluster) LeaderAddr() string { ... }
What's missing
StatusResponse has no leadership fields:
// internal/hub/types.go:264-269 type StatusResponse struct { TotalEntries uint64 `json:"total_entries"` ConnectedClients uint32 `json:"connected_clients"` EntriesByType map[string]uint64 `json:"entries_by_type"` EntriesByProject map[string]uint64 `json:"entries_by_project"` }
The Status handler (internal/hub/handler.go) populates these from Store but never touches s.cluster. The ctx hub status CLI renders whatever the RPC returns, so even if an operator runs the cluster in HA mode, the CLI is silent about it.
Proposed Shape
Three small changes, all in this order so each is reviewable:
1. Extend StatusResponse
// internal/hub/types.go type StatusResponse struct { TotalEntries uint64 `json:"total_entries"` ConnectedClients uint32 `json:"connected_clients"` EntriesByType map[string]uint64 `json:"entries_by_type"` EntriesByProject map[string]uint64 `json:"entries_by_project"` // Cluster fields are zero values when the hub runs // standalone (no Raft peers configured). ClusterEnabled bool `json:"cluster_enabled"` IsLeader bool `json:"is_leader,omitempty"` LeaderAddr string `json:"leader_addr,omitempty"` }
ClusterEnabled is the disambiguator: when false, the other two are meaningless zero values; when true, they reflect live Raft state. Without ClusterEnabled, a standalone hub would always report IsLeader=false, LeaderAddr="" which would mislead anyone reading the response.
2. Populate in the handler
// internal/hub/handler.go (Status handler) resp := &StatusResponse{ TotalEntries: ..., ConnectedClients: ..., EntriesByType: ..., EntriesByProject: ..., } if s.cluster != nil { resp.ClusterEnabled = true resp.IsLeader = s.cluster.IsLeader() resp.LeaderAddr = s.cluster.LeaderAddr() } return resp, nil
3. Render in the CLI
Locate the ctx hub status rendering layer (probably internal/cli/hub/core/server/render.go or similar; grep for the existing TotalEntries rendering to find it) and add a short cluster section:
Cluster: enabled (leader: 10.0.0.5:7081)
this node IS the leader
or, when not in cluster mode:
Cluster: standalone
Use the existing rendering style for consistency.
Tests Required
TestStatus_StandaloneReportsClusterDisabled: start a Server withoutSetCluster, call Status, assertClusterEnabled == false.TestStatus_ClusterReportsLeadershipState: start a single-node Raft cluster, wait for it to elect itself leader, call Status, assertClusterEnabled == true && IsLeader == true && LeaderAddr != "".- (Render layer) Snapshot/golden-file test for the two CLI rendering shapes (standalone vs cluster).
Out of Scope
- Adding a
Leadershipstreaming RPC (operator could subscribe to leadership changes). The pull-via-Status surface is the minimum useful; streaming can come later if a real use case appears. - A
ctx hub leadershortcut command.ctx hub statusreturning the info is sufficient; a dedicated subcommand is sugar. - Tracking the Raft term / commit-index / log-position. Useful for debugging quorum issues but a separate concern from "who is leader right now".
Acceptance
-
StatusResponsegainsClusterEnabled,IsLeader,LeaderAddrfields. - Status handler populates them from
s.clusterwhen present. -
ctx hub statusrenders the cluster section. - Tests for standalone and cluster modes both pass.
Scope for "good first issue"
This is wiring an existing data source (Cluster.IsLeader(), Cluster.LeaderAddr()) through three layers (response struct → handler → CLI render) plus a small test. No new gRPC method, no auth concerns, no concurrency surprise (the Cluster has its own locking). A newcomer can pattern-match against how TotalEntries flows from Store → handler → CLI to land this.