File
Blob: chord/overview_handler.go
| 1 | package chord |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "net/http" |
| 6 | "strconv" |
| 7 | "time" |
| 8 | |
| 9 | "go.miragespace.co/specter/spec/protocol" |
| 10 | ) |
| 11 | |
| 12 | type overviewIdentity struct { |
| 13 | ID string `json:"id"` |
| 14 | Address string `json:"address"` |
| 15 | } |
| 16 | |
| 17 | type overviewVNode struct { |
| 18 | Identity overviewIdentity `json:"identity"` |
| 19 | Root bool `json:"root"` |
| 20 | State string `json:"state"` |
| 21 | LastStabilized *time.Time `json:"last_stabilized"` |
| 22 | ChordRequests uint64 `json:"chord_requests"` |
| 23 | KVRequests uint64 `json:"kv_requests"` |
| 24 | RPCErrors uint64 `json:"rpc_errors"` |
| 25 | KVStale uint64 `json:"kv_stale"` |
| 26 | PredecessorAvailable bool `json:"predecessor_available"` |
| 27 | Predecessor *overviewIdentity `json:"predecessor"` |
| 28 | SuccessorsAvailable bool `json:"successors_available"` |
| 29 | Successors []overviewIdentity `json:"successors"` |
| 30 | } |
| 31 | |
| 32 | type localOverview struct { |
| 33 | ObservedAt time.Time `json:"observed_at"` |
| 34 | Identity overviewIdentity `json:"identity"` |
| 35 | Provider string `json:"provider"` |
| 36 | VNodes []overviewVNode `json:"vnodes"` |
| 37 | } |
| 38 | |
| 39 | func overviewNodeIdentity(node *protocol.Node) overviewIdentity { |
| 40 | return overviewIdentity{ |
| 41 | ID: strconv.FormatUint(node.GetId(), 10), |
| 42 | Address: node.GetAddress(), |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | // snapshotOverview samples local counters and membership references independently. |
| 47 | // It never consults storage or calls peers. Membership changes can hold locks |
| 48 | // across network requests, so a busy reference is reported as unavailable. |
| 49 | func snapshotOverview(rootNode *LocalNode, virtualNodes []*LocalNode, provider string) localOverview { |
| 50 | info := localOverview{ |
| 51 | ObservedAt: time.Now().UTC(), |
| 52 | Identity: overviewNodeIdentity(rootNode.Identity()), |
| 53 | Provider: provider, |
| 54 | VNodes: make([]overviewVNode, 0, len(virtualNodes)), |
| 55 | } |
| 56 | for _, node := range virtualNodes { |
| 57 | vnode := overviewVNode{ |
| 58 | Identity: overviewNodeIdentity(node.Identity()), |
| 59 | Root: node == rootNode, |
| 60 | State: node.state.Get().String(), |
| 61 | ChordRequests: node.chordRate.Total(), |
| 62 | KVRequests: node.kvRate.Total(), |
| 63 | RPCErrors: node.rpcErrorCount.Load(), |
| 64 | KVStale: node.kvStaleCount.Load(), |
| 65 | Successors: make([]overviewIdentity, 0), |
| 66 | } |
| 67 | if stabilized := node.lastStabilized.Load(); !stabilized.IsZero() { |
| 68 | vnode.LastStabilized = &stabilized |
| 69 | } |
| 70 | if node.predecessorMu.TryRLock() { |
| 71 | vnode.PredecessorAvailable = true |
| 72 | if node.predecessor != nil { |
| 73 | predecessor := overviewNodeIdentity(node.predecessor.Identity()) |
| 74 | vnode.Predecessor = &predecessor |
| 75 | } |
| 76 | node.predecessorMu.RUnlock() |
| 77 | } |
| 78 | if node.successorsMu.TryRLock() { |
| 79 | vnode.SuccessorsAvailable = true |
| 80 | for _, successor := range node.successors { |
| 81 | if successor != nil { |
| 82 | vnode.Successors = append(vnode.Successors, overviewNodeIdentity(successor.Identity())) |
| 83 | } |
| 84 | } |
| 85 | node.successorsMu.RUnlock() |
| 86 | } |
| 87 | info.VNodes = append(info.VNodes, vnode) |
| 88 | } |
| 89 | return info |
| 90 | } |
| 91 | |
| 92 | // OverviewHandler serves a bounded local JSON overview. The composition layer supplies |
| 93 | // the effective provider name; the diagnostic snapshot does not identify storage |
| 94 | // implementations or inspect their data directories. |
| 95 | // |
| 96 | // Mount this behind the same authentication as the other operator handlers at |
| 97 | // /_internal/overview.json. |
| 98 | func OverviewHandler(rootNode *LocalNode, virtualNodes []*LocalNode, provider string) http.Handler { |
| 99 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 100 | info := snapshotOverview(rootNode, virtualNodes, provider) |
| 101 | w.Header().Set("Cache-Control", "no-store") |
| 102 | w.Header().Set("Content-Type", "application/json; charset=utf-8") |
| 103 | json.NewEncoder(w).Encode(info) |
| 104 | }) |
| 105 | } |