mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
119 lines
3.1 KiB
Go
119 lines
3.1 KiB
Go
package machine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
"github.com/psviderski/uncloud/internal/cli"
|
|
"github.com/psviderski/uncloud/internal/cli/tui"
|
|
"github.com/spf13/cobra"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
func NewRTTCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "rtt",
|
|
Short: "Show round-trip times between machines.",
|
|
Long: `Show round-trip times between machines.
|
|
|
|
Round-trip time statistics are collected from the Corrosion gossip protocol
|
|
and represent the median of recent RTT samples between each pair of machines
|
|
in the cluster. The values shown include the median RTT and standard deviation
|
|
for each machine-to-machine connection.`,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
uncli := cmd.Context().Value("cli").(*cli.CLI)
|
|
return rtt(cmd.Context(), uncli)
|
|
},
|
|
}
|
|
return cmd
|
|
}
|
|
|
|
func rtt(ctx context.Context, uncli *cli.CLI) error {
|
|
client, err := uncli.ConnectCluster(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("connect to cluster: %w", err)
|
|
}
|
|
defer client.Close()
|
|
|
|
// Setup context to proxy request to all machines.
|
|
ctx = client.ProxyMachinesContext(ctx, nil)
|
|
|
|
resp, err := client.MachineClient.InspectMachine(ctx, &emptypb.Empty{})
|
|
if err != nil {
|
|
return fmt.Errorf("inspect machines: %w", err)
|
|
}
|
|
|
|
// Map machine IDs to names for display from the response.
|
|
machineNames := make(map[string]string)
|
|
for _, m := range resp.Machines {
|
|
// NOTE: Metadata should never be nil in practice. This is legacy fallback that will be removed.
|
|
if m.Metadata == nil {
|
|
tui.PrintWarning("metadata is missing in response from unknown server")
|
|
continue
|
|
}
|
|
if m.Metadata.Error != "" {
|
|
tui.PrintWarning(fmt.Sprintf("failed to inspect machine '%s': %s", m.Metadata.MachineName, m.Metadata.Error))
|
|
continue
|
|
}
|
|
if m.Machine == nil {
|
|
continue
|
|
}
|
|
machineNames[m.Machine.Id] = m.Machine.Name
|
|
}
|
|
|
|
type row struct {
|
|
machine string
|
|
peer string
|
|
median time.Duration
|
|
stdDev time.Duration
|
|
}
|
|
var rows []row
|
|
|
|
for _, m := range resp.Machines {
|
|
// Unlikely to occur, but might be a possible edge case when
|
|
// a machine is still initializing. So just to be safe.
|
|
if m.Machine == nil || m.Rtts == nil {
|
|
continue
|
|
}
|
|
for peerID, stats := range m.Rtts {
|
|
peerName := peerID
|
|
if name, ok := machineNames[peerID]; ok {
|
|
peerName = name
|
|
}
|
|
rows = append(rows, row{
|
|
machine: m.Machine.Name,
|
|
peer: peerName,
|
|
median: stats.Median.AsDuration(),
|
|
stdDev: stats.StdDev.AsDuration(),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort by machine name then peer name.
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
if rows[i].machine == rows[j].machine {
|
|
return rows[i].peer < rows[j].peer
|
|
}
|
|
return rows[i].machine < rows[j].machine
|
|
})
|
|
|
|
// Print table.
|
|
t := tui.NewTable()
|
|
t.Headers("MACHINE", "PEER", "MEDIAN", "STDDEV")
|
|
|
|
for _, r := range rows {
|
|
t.Row(r.machine, r.peer, tui.FormatRTT(r.median), formatRTTStdDev(r.stdDev))
|
|
}
|
|
|
|
lipgloss.Println(t)
|
|
return nil
|
|
}
|
|
|
|
// formatRTTStdDev formats a round-trip time standard deviation with one decimal place, e.g. "±19.4ms".
|
|
func formatRTTStdDev(d time.Duration) string {
|
|
return fmt.Sprintf("±%.1fms", float64(d)/float64(time.Millisecond))
|
|
}
|