Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Hub status: surface cluster leadership (IsLeader, LeaderAddr) in Status RPC + ctx hub status #96

Open
Labels
bugSomething isn't working good first issueGood for newcomers

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 without SetCluster, call Status, assert ClusterEnabled == false.
  • TestStatus_ClusterReportsLeadershipState: start a single-node Raft cluster, wait for it to elect itself leader, call Status, assert ClusterEnabled == true && IsLeader == true && LeaderAddr != "".
  • (Render layer) Snapshot/golden-file test for the two CLI rendering shapes (standalone vs cluster).

Out of Scope

  • Adding a Leadership streaming 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 leader shortcut command. ctx hub status returning 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

  • StatusResponse gains ClusterEnabled, IsLeader, LeaderAddr fields.
  • Status handler populates them from s.cluster when present.
  • ctx hub status renders 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working good first issueGood for newcomers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /