mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 22:26:58 +00:00
146 lines
3.7 KiB
Go
146 lines
3.7 KiB
Go
package lifecycle
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// checkQuorumSafety queries local RQLite to determine if stopping this node
|
|
// would break quorum. Returns a warning message if unsafe, empty string if safe.
|
|
func checkQuorumSafety() string {
|
|
// Query local RQLite status to check if we're a voter
|
|
status, err := getLocalRQLiteStatus()
|
|
if err != nil {
|
|
// RQLite may not be running — safe to stop
|
|
return ""
|
|
}
|
|
|
|
raftState, _ := status["state"].(string)
|
|
isVoter, _ := status["voter"].(bool)
|
|
|
|
// If we're not a voter, stopping is always safe for quorum
|
|
if !isVoter {
|
|
return ""
|
|
}
|
|
|
|
// Query /nodes to count reachable voters
|
|
nodes, err := getLocalRQLiteNodes()
|
|
if err != nil {
|
|
return fmt.Sprintf("Cannot verify quorum safety (failed to query nodes: %v). This node is a %s voter.", err, raftState)
|
|
}
|
|
|
|
reachableVoters := 0
|
|
totalVoters := 0
|
|
for _, node := range nodes {
|
|
voter, _ := node["voter"].(bool)
|
|
reachable, _ := node["reachable"].(bool)
|
|
if voter {
|
|
totalVoters++
|
|
if reachable {
|
|
reachableVoters++
|
|
}
|
|
}
|
|
}
|
|
|
|
// After removing this voter, remaining voters must form quorum:
|
|
// quorum = (totalVoters / 2) + 1, so we need reachableVoters - 1 >= quorum
|
|
remainingVoters := reachableVoters - 1
|
|
quorumNeeded := (totalVoters-1)/2 + 1
|
|
|
|
if remainingVoters < quorumNeeded {
|
|
role := raftState
|
|
if role == "Leader" {
|
|
role = "the LEADER"
|
|
}
|
|
return fmt.Sprintf(
|
|
"Stopping this node (%s, %s) would break RQLite quorum (%d/%d reachable voters would remain, need %d).",
|
|
role, "voter", remainingVoters, totalVoters-1, quorumNeeded)
|
|
}
|
|
|
|
if raftState == "Leader" {
|
|
// Not quorum-breaking but warn about leadership
|
|
fmt.Printf(" Note: This node is the RQLite leader. Leadership will transfer on shutdown.\n")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// getLocalRQLiteStatus queries local RQLite /status and extracts raft info
|
|
func getLocalRQLiteStatus() (map[string]interface{}, error) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Get("http://localhost:5001/status")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var status map[string]interface{}
|
|
if err := json.Unmarshal(body, &status); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract raft state from nested structure
|
|
store, _ := status["store"].(map[string]interface{})
|
|
if store == nil {
|
|
return nil, fmt.Errorf("no store in status")
|
|
}
|
|
raft, _ := store["raft"].(map[string]interface{})
|
|
if raft == nil {
|
|
return nil, fmt.Errorf("no raft in status")
|
|
}
|
|
|
|
// Add voter status from the node info
|
|
result := map[string]interface{}{
|
|
"state": raft["state"],
|
|
"voter": true, // Local node queries /status which doesn't include voter flag, assume voter if we got here
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// getLocalRQLiteNodes queries local RQLite /nodes?nonvoters to get cluster members
|
|
func getLocalRQLiteNodes() ([]map[string]interface{}, error) {
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Get("http://localhost:5001/nodes?nonvoters&timeout=3s")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// RQLite /nodes returns a map of node_id -> node_info
|
|
var nodesMap map[string]map[string]interface{}
|
|
if err := json.Unmarshal(body, &nodesMap); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var nodes []map[string]interface{}
|
|
for _, node := range nodesMap {
|
|
nodes = append(nodes, node)
|
|
}
|
|
|
|
return nodes, nil
|
|
}
|
|
|
|
// containsService checks if a service name exists in the service list
|
|
func containsService(services []string, name string) bool {
|
|
for _, s := range services {
|
|
if s == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|