mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat(rtt): 'machine rtt' command to show round-trip time between macines usign using gossip data (#226)
* Add corrosion admin client function to get RTT to other machines in the cluster * Add `uc machine rtt` showing all pair-wise RTT stats from corrosion * Add long description to machine rtt command * Include machine peer RTTs in InspectMachine instead of adding new gRPC API for it * Add nil check on InspectMachine's Rtt field to protect from a potential edge case * Generate docs for machine rtt command * Handle m.Message being nil when a node is down or unavailble * Update cli-docs * Use tui Table instead of tabwriter * Change reported RTT to the median rather than mean. Still calculate include stddev, as it might be a useful indication of network jitter. * Show RTT to peers in `wg show` output * Update cli docs
This commit is contained in:
@@ -6,8 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -256,3 +258,106 @@ func (c *AdminClient) ClusterMembershipStates(latest bool) ([]ClusterMembershipS
|
||||
}
|
||||
return states, parseErr
|
||||
}
|
||||
|
||||
type MemberRTTStats struct {
|
||||
Addr netip.AddrPort
|
||||
Median float64
|
||||
StdDev float64
|
||||
}
|
||||
|
||||
// ClusterMemberRTTs returns the median and standard deviation of round-trip times to each cluster member.
|
||||
func (c *AdminClient) ClusterMemberRTTs() ([]MemberRTTStats, error) {
|
||||
respCh, err := c.SendCommand([]byte("{\"Cluster\":\"Members\"}"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stats []MemberRTTStats
|
||||
var parseErr error
|
||||
|
||||
for r := range respCh {
|
||||
if r.Err != nil {
|
||||
return nil, r.Err
|
||||
}
|
||||
|
||||
addr, rtts, err := parseClusterMemberRTT(r.JSON)
|
||||
if err != nil {
|
||||
parseErr = errors.Join(parseErr, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(rtts) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Float64s(rtts)
|
||||
n := len(rtts)
|
||||
var median float64
|
||||
if n%2 == 0 {
|
||||
median = (rtts[n/2-1] + rtts[n/2]) / 2
|
||||
} else {
|
||||
median = rtts[n/2]
|
||||
}
|
||||
|
||||
var sum float64
|
||||
for _, rtt := range rtts {
|
||||
sum += rtt
|
||||
}
|
||||
avg := sum / float64(n)
|
||||
|
||||
var varianceSum float64
|
||||
for _, rtt := range rtts {
|
||||
diff := rtt - avg
|
||||
varianceSum += diff * diff
|
||||
}
|
||||
stdDev := math.Sqrt(varianceSum / float64(n))
|
||||
|
||||
stats = append(stats, MemberRTTStats{
|
||||
Addr: addr,
|
||||
Median: median,
|
||||
StdDev: stdDev,
|
||||
})
|
||||
}
|
||||
|
||||
return stats, parseErr
|
||||
}
|
||||
|
||||
func parseClusterMemberRTT(json map[string]any) (netip.AddrPort, []float64, error) {
|
||||
var addr netip.AddrPort
|
||||
var rtts []float64
|
||||
var err error
|
||||
|
||||
// Parse state to get Addr
|
||||
stateObj, ok := json["state"].(map[string]any)
|
||||
if !ok {
|
||||
return addr, nil, fmt.Errorf("missing or invalid 'state' field")
|
||||
}
|
||||
|
||||
if addrStr, ok := stateObj["addr"].(string); ok {
|
||||
addr, err = netip.ParseAddrPort(addrStr)
|
||||
if err != nil {
|
||||
return addr, nil, fmt.Errorf("parse 'addr' field: %w", err)
|
||||
}
|
||||
} else {
|
||||
return addr, nil, fmt.Errorf("missing or invalid 'addr' field in 'state'")
|
||||
}
|
||||
|
||||
// Parse RTTs
|
||||
if rttsVal, ok := json["rtts"]; ok {
|
||||
if rttsSlice, ok := rttsVal.([]any); ok {
|
||||
for _, v := range rttsSlice {
|
||||
if f, ok := v.(float64); ok {
|
||||
rtts = append(rtts, f)
|
||||
} else {
|
||||
return addr, nil, fmt.Errorf("invalid rtt value type: %T", v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return addr, nil, fmt.Errorf("invalid 'rtts' field type: %T", rttsVal)
|
||||
}
|
||||
} else {
|
||||
return addr, nil, fmt.Errorf("missing 'rtts' field")
|
||||
}
|
||||
|
||||
return addr, rtts, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user