diff --git a/cmd/uncloud/machine/rtt.go b/cmd/uncloud/machine/rtt.go index 02616e85..2a0d51eb 100644 --- a/cmd/uncloud/machine/rtt.go +++ b/cmd/uncloud/machine/rtt.go @@ -3,8 +3,8 @@ package machine import ( "context" "fmt" - "math" "sort" + "time" "github.com/psviderski/uncloud/internal/cli" "github.com/psviderski/uncloud/internal/cli/tui" @@ -60,14 +60,14 @@ func rtt(ctx context.Context, uncli *cli.CLI) error { type row struct { machine string peer string - median float64 - stdDev float64 + 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.. + // a machine is still initializing. So just to be safe. if m.Machine == nil || m.Rtts == nil { continue } @@ -79,13 +79,13 @@ func rtt(ctx context.Context, uncli *cli.CLI) error { rows = append(rows, row{ machine: m.Machine.Name, peer: peerName, - median: stats.Median, - stdDev: stats.StdDev, + median: stats.Median.AsDuration(), + stdDev: stats.StdDev.AsDuration(), }) } } - // Sort by machine name then peer name + // 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 @@ -93,14 +93,19 @@ func rtt(ctx context.Context, uncli *cli.CLI) error { return rows[i].machine < rows[j].machine }) - // Print table + // Print table. t := tui.NewTable() t.Headers("MACHINE", "PEER", "MEDIAN", "STDDEV") for _, r := range rows { - t.Row(r.machine, r.peer, fmt.Sprintf("%dms", int64(math.Ceil(r.median))), fmt.Sprintf("±%.1fms", r.stdDev)) + t.Row(r.machine, r.peer, tui.FormatRTT(r.median), formatRTTStdDev(r.stdDev)) } fmt.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)) +} diff --git a/cmd/uncloud/wg/wg.go b/cmd/uncloud/wg/wg.go index f68ca2c3..0c921584 100644 --- a/cmd/uncloud/wg/wg.go +++ b/cmd/uncloud/wg/wg.go @@ -3,7 +3,6 @@ package wg import ( "context" "fmt" - "math" "strings" "time" @@ -82,7 +81,7 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error { machinesByPublicKey[publicKey] = m.Machine } - // Fetch the machine's info and RTTs for display + // Fetch the machine's info and RTTs for display. var selfMachine *pb.MachineDetails inspectResp, err := client.MachineClient.InspectMachine(ctx, nil) if err == nil { @@ -111,7 +110,7 @@ func runShow(ctx context.Context, uncli *cli.CLI, opts showOptions) error { machineName = m.Name if selfMachine != nil { if stats, ok := selfMachine.Rtts[m.Id]; ok { - rtt = fmt.Sprintf("%dms", int64(math.Ceil(stats.Median))) + rtt = tui.FormatRTT(stats.Median.AsDuration()) } } } diff --git a/internal/cli/tui/format.go b/internal/cli/tui/format.go new file mode 100644 index 00000000..bfe77eb2 --- /dev/null +++ b/internal/cli/tui/format.go @@ -0,0 +1,12 @@ +package tui + +import ( + "fmt" + "math" + "time" +) + +// FormatRTT formats a round-trip time duration as whole milliseconds, e.g. "140ms". +func FormatRTT(d time.Duration) string { + return fmt.Sprintf("%dms", int64(math.Round(float64(d)/float64(time.Millisecond)))) +} diff --git a/internal/cli/tui/format_test.go b/internal/cli/tui/format_test.go new file mode 100644 index 00000000..28540258 --- /dev/null +++ b/internal/cli/tui/format_test.go @@ -0,0 +1,32 @@ +package tui + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestFormatRTT(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + in time.Duration + want string + }{ + {"zero", 0, "0ms"}, + {"rounds down", 39*time.Millisecond + 200*time.Microsecond, "39ms"}, + {"rounds up at half", 39*time.Millisecond + 500*time.Microsecond, "40ms"}, + {"rounds up near next ms", 39*time.Millisecond + 600*time.Microsecond, "40ms"}, + {"exact ms", 140 * time.Millisecond, "140ms"}, + {"one second", time.Second, "1000ms"}, + {"1.5s", 1500 * time.Millisecond, "1500ms"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, FormatRTT(tt.in)) + }) + } +} diff --git a/internal/corrosion/admin.go b/internal/corrosion/admin.go index f2733f5f..bef67b07 100644 --- a/internal/corrosion/admin.go +++ b/internal/corrosion/admin.go @@ -261,8 +261,8 @@ func (c *AdminClient) ClusterMembershipStates(latest bool) ([]ClusterMembershipS type MemberRTTStats struct { Addr netip.AddrPort - Median float64 - StdDev float64 + Median time.Duration + StdDev time.Duration } // ClusterMemberRTTs returns the median and standard deviation of round-trip times to each cluster member. @@ -290,44 +290,54 @@ func (c *AdminClient) ClusterMemberRTTs() ([]MemberRTTStats, error) { 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)) - + // Corrosion reports RTT samples as floating-point milliseconds. + medianMs, stdDevMs := computeRTTStatsMs(rtts) stats = append(stats, MemberRTTStats{ Addr: addr, - Median: median, - StdDev: stdDev, + Median: time.Duration(medianMs * float64(time.Millisecond)), + StdDev: time.Duration(stdDevMs * float64(time.Millisecond)), }) } return stats, parseErr } +// computeRTTStatsMs returns the median and population standard deviation (ms) of the given samples. +func computeRTTStatsMs(rtts []float64) (median, stdDev float64) { + if len(rtts) == 0 { + return 0, 0 + } + + sort.Float64s(rtts) + n := len(rtts) + 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)) + + return median, stdDev +} + 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 + // Parse state to get Addr. stateObj, ok := json["state"].(map[string]any) if !ok { return addr, nil, fmt.Errorf("missing or invalid 'state' field") @@ -342,21 +352,21 @@ func parseClusterMemberRTT(json map[string]any) (netip.AddrPort, []float64, erro 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) + // The absent 'rtts' key or equal to null are treated as no samples yet. + rttsVal, ok := json["rtts"] + if !ok || rttsVal == nil { + return addr, nil, nil + } + rttsSlice, ok := rttsVal.([]any) + if !ok { + return addr, nil, fmt.Errorf("invalid 'rtts' field type: %T", rttsVal) + } + for _, v := range rttsSlice { + f, ok := v.(float64) + if !ok { + return addr, nil, fmt.Errorf("invalid rtt value type: %T", v) } - } else { - return addr, nil, fmt.Errorf("missing 'rtts' field") + rtts = append(rtts, f) } return addr, rtts, nil diff --git a/internal/corrosion/admin_test.go b/internal/corrosion/admin_test.go new file mode 100644 index 00000000..7aff03d8 --- /dev/null +++ b/internal/corrosion/admin_test.go @@ -0,0 +1,174 @@ +package corrosion + +import ( + "math" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestComputeRTTStatsMs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + samples []float64 + wantMedian float64 + wantStdDev float64 + }{ + { + name: "single sample", + samples: []float64{42}, + wantMedian: 42, + wantStdDev: 0, + }, + { + name: "two samples (even, averaged)", + samples: []float64{10, 20}, + wantMedian: 15, + // Population stddev: variance = ((10-15)^2 + (20-15)^2)/2 = 25; sqrt = 5. + wantStdDev: 5, + }, + { + name: "three samples (odd, middle)", + samples: []float64{3, 1, 2}, + wantMedian: 2, + // Mean = 2; variance = (1+0+1)/3 = 0.666...; stddev = sqrt(2/3). + wantStdDev: math.Sqrt(2.0 / 3.0), + }, + { + name: "four samples (even, averaged)", + samples: []float64{1, 2, 3, 4}, + wantMedian: 2.5, + // Mean = 2.5; variance = (2.25+0.25+0.25+2.25)/4 = 1.25; stddev = sqrt(1.25). + wantStdDev: math.Sqrt(1.25), + }, + { + // Verifies that unsorted input is sorted before picking the median. + name: "unsorted input", + samples: []float64{100, 1, 50}, + wantMedian: 50, + // Mean = 151/3. Variance = ((149^2 + 148^2 + 1^2) / 9) / 3. + wantStdDev: math.Sqrt(float64(149*149+148*148+1) / 9.0 / 3.0), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + median, stdDev := computeRTTStatsMs(tt.samples) + assert.InDelta(t, tt.wantMedian, median, 1e-9, "median") + assert.InDelta(t, tt.wantStdDev, stdDev, 1e-9, "stdDev") + }) + } +} + +func TestParseClusterMemberRTT(t *testing.T) { + validState := map[string]any{"addr": "[fdcc:b618:5034:7afa:172a:1452:f2de:3c99]:51001"} + + tests := []struct { + name string + input map[string]any + wantAddr string + wantRTTs []float64 + wantErr bool + errSubstr string + }{ + { + name: "valid state and rtts", + input: map[string]any{ + "state": validState, + "rtts": []any{float64(10), float64(20), float64(30)}, + }, + wantAddr: "[fdcc:b618:5034:7afa:172a:1452:f2de:3c99]:51001", + wantRTTs: []float64{10, 20, 30}, + }, + { + name: "missing rtts key is not an error", + input: map[string]any{ + "state": validState, + }, + wantAddr: "[fdcc:b618:5034:7afa:172a:1452:f2de:3c99]:51001", + wantRTTs: nil, + }, + { + name: "null rtts value is not an error", + input: map[string]any{ + "state": validState, + "rtts": nil, + }, + wantAddr: "[fdcc:b618:5034:7afa:172a:1452:f2de:3c99]:51001", + wantRTTs: nil, + }, + { + name: "empty rtts array is not an error", + input: map[string]any{ + "state": validState, + "rtts": []any{}, + }, + wantAddr: "[fdcc:b618:5034:7afa:172a:1452:f2de:3c99]:51001", + wantRTTs: nil, + }, + { + name: "non-array rtts", + input: map[string]any{ + "state": validState, + "rtts": "not-an-array", + }, + wantErr: true, + errSubstr: "invalid 'rtts' field type", + }, + { + name: "non-number element in rtts", + input: map[string]any{ + "state": validState, + "rtts": []any{float64(10), "bad"}, + }, + wantErr: true, + errSubstr: "invalid rtt value type", + }, + { + name: "missing state", + input: map[string]any{"rtts": []any{float64(1)}}, + wantErr: true, + errSubstr: "missing or invalid 'state' field", + }, + { + name: "missing addr in state", + input: map[string]any{ + "state": map[string]any{}, + "rtts": []any{float64(1)}, + }, + wantErr: true, + errSubstr: "missing or invalid 'addr' field", + }, + { + name: "invalid addr format", + input: map[string]any{ + "state": map[string]any{"addr": "not-an-addr"}, + "rtts": []any{float64(1)}, + }, + wantErr: true, + errSubstr: "parse 'addr' field", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, rtts, err := parseClusterMemberRTT(tt.input) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errSubstr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantAddr, addr.String()) + assert.Equal(t, tt.wantRTTs, rtts) + + // Sanity: the parsed addr is a valid AddrPort. + _, perr := netip.ParseAddrPort(addr.String()) + assert.NoError(t, perr) + }) + } +} diff --git a/internal/machine/api/pb/machine.pb.go b/internal/machine/api/pb/machine.pb.go index f8f95205..5baf38e2 100644 --- a/internal/machine/api/pb/machine.pb.go +++ b/internal/machine/api/pb/machine.pb.go @@ -9,6 +9,7 @@ package pb import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" emptypb "google.golang.org/protobuf/types/known/emptypb" timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" @@ -972,8 +973,8 @@ type RTTStats struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Median float64 `protobuf:"fixed64,1,opt,name=median,proto3" json:"median,omitempty"` - StdDev float64 `protobuf:"fixed64,2,opt,name=std_dev,json=stdDev,proto3" json:"std_dev,omitempty"` + Median *durationpb.Duration `protobuf:"bytes,1,opt,name=median,proto3" json:"median,omitempty"` + StdDev *durationpb.Duration `protobuf:"bytes,2,opt,name=std_dev,json=stdDev,proto3" json:"std_dev,omitempty"` } func (x *RTTStats) Reset() { @@ -1008,18 +1009,18 @@ func (*RTTStats) Descriptor() ([]byte, []int) { return file_internal_machine_api_pb_machine_proto_rawDescGZIP(), []int{15} } -func (x *RTTStats) GetMedian() float64 { +func (x *RTTStats) GetMedian() *durationpb.Duration { if x != nil { return x.Median } - return 0 + return nil } -func (x *RTTStats) GetStdDev() float64 { +func (x *RTTStats) GetStdDev() *durationpb.Duration { if x != nil { return x.StdDev } - return 0 + return nil } type Service_Container struct { @@ -1083,7 +1084,9 @@ var File_internal_machine_api_pb_machine_proto protoreflect.FileDescriptor var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{ 0x0a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1b, 0x67, 0x6f, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x1a, 0x1e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, @@ -1217,57 +1220,60 @@ var file_internal_machine_api_pb_machine_proto_rawDesc = []byte{ 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6d, 0x69, 0x74, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x22, 0x3b, 0x0a, 0x08, - 0x52, 0x54, 0x54, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x64, 0x69, - 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x06, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, - 0x12, 0x17, 0x0a, 0x07, 0x73, 0x74, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x01, 0x52, 0x06, 0x73, 0x74, 0x64, 0x44, 0x65, 0x76, 0x32, 0x95, 0x05, 0x0a, 0x07, 0x4d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, - 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, - 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, - 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, - 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, - 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, - 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, - 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, - 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x22, 0x71, 0x0a, 0x08, + 0x52, 0x54, 0x54, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x06, 0x6d, 0x65, 0x64, 0x69, + 0x61, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x12, 0x32, 0x0a, 0x07, 0x73, + 0x74, 0x64, 0x5f, 0x64, 0x65, 0x76, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x73, 0x74, 0x64, 0x44, 0x65, 0x76, 0x32, + 0x95, 0x05, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x4d, 0x0a, 0x12, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, 0x65, + 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1f, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x50, 0x72, 0x65, 0x72, 0x65, 0x71, 0x75, 0x69, 0x73, 0x69, 0x74, + 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x49, 0x6e, + 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6c, 0x75, + 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0b, + 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x4a, 0x6f, 0x69, 0x6e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x33, 0x0a, 0x05, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x33, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, - 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x30, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, - 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x45, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, + 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, + 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, + 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x24, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x57, 0x69, + 0x72, 0x65, 0x47, 0x75, 0x61, 0x72, 0x64, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x52, 0x65, 0x73, 0x65, 0x74, 0x12, + 0x11, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x49, 0x0a, 0x0e, 0x49, 0x6e, + 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x6f, 0x67, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x30, 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x73, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x6b, 0x69, + 0x2f, 0x75, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1307,9 +1313,10 @@ var file_internal_machine_api_pb_machine_proto_goTypes = []any{ (*IPPort)(nil), // 20: api.IPPort (*Metadata)(nil), // 21: api.Metadata (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp - (*emptypb.Empty)(nil), // 23: google.protobuf.Empty - (*LogsRequest)(nil), // 24: api.LogsRequest - (*LogEntry)(nil), // 25: api.LogEntry + (*durationpb.Duration)(nil), // 23: google.protobuf.Duration + (*emptypb.Empty)(nil), // 24: google.protobuf.Empty + (*LogsRequest)(nil), // 25: api.LogsRequest + (*LogEntry)(nil), // 26: api.LogEntry } var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ 1, // 0: api.MachineInfo.network:type_name -> api.NetworkConfig @@ -1331,32 +1338,34 @@ var file_internal_machine_api_pb_machine_proto_depIdxs = []int32{ 10, // 16: api.InspectServiceResponse.service:type_name -> api.Service 14, // 17: api.InspectWireGuardNetworkResponse.peers:type_name -> api.WireGuardPeer 22, // 18: api.WireGuardPeer.last_handshake_time:type_name -> google.protobuf.Timestamp - 15, // 19: api.MachineDetails.RttsEntry.value:type_name -> api.RTTStats - 23, // 20: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty - 3, // 21: api.Machine.InitCluster:input_type -> api.InitClusterRequest - 5, // 22: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest - 23, // 23: api.Machine.Token:input_type -> google.protobuf.Empty - 23, // 24: api.Machine.Inspect:input_type -> google.protobuf.Empty - 23, // 25: api.Machine.InspectMachine:input_type -> google.protobuf.Empty - 23, // 26: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty - 9, // 27: api.Machine.Reset:input_type -> api.ResetRequest - 11, // 28: api.Machine.InspectService:input_type -> api.InspectServiceRequest - 24, // 29: api.Machine.MachineLogs:input_type -> api.LogsRequest - 2, // 30: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse - 4, // 31: api.Machine.InitCluster:output_type -> api.InitClusterResponse - 23, // 32: api.Machine.JoinCluster:output_type -> google.protobuf.Empty - 8, // 33: api.Machine.Token:output_type -> api.TokenResponse - 0, // 34: api.Machine.Inspect:output_type -> api.MachineInfo - 6, // 35: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse - 13, // 36: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse - 23, // 37: api.Machine.Reset:output_type -> google.protobuf.Empty - 12, // 38: api.Machine.InspectService:output_type -> api.InspectServiceResponse - 25, // 39: api.Machine.MachineLogs:output_type -> api.LogEntry - 30, // [30:40] is the sub-list for method output_type - 20, // [20:30] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 23, // 19: api.RTTStats.median:type_name -> google.protobuf.Duration + 23, // 20: api.RTTStats.std_dev:type_name -> google.protobuf.Duration + 15, // 21: api.MachineDetails.RttsEntry.value:type_name -> api.RTTStats + 24, // 22: api.Machine.CheckPrerequisites:input_type -> google.protobuf.Empty + 3, // 23: api.Machine.InitCluster:input_type -> api.InitClusterRequest + 5, // 24: api.Machine.JoinCluster:input_type -> api.JoinClusterRequest + 24, // 25: api.Machine.Token:input_type -> google.protobuf.Empty + 24, // 26: api.Machine.Inspect:input_type -> google.protobuf.Empty + 24, // 27: api.Machine.InspectMachine:input_type -> google.protobuf.Empty + 24, // 28: api.Machine.InspectWireGuardNetwork:input_type -> google.protobuf.Empty + 9, // 29: api.Machine.Reset:input_type -> api.ResetRequest + 11, // 30: api.Machine.InspectService:input_type -> api.InspectServiceRequest + 25, // 31: api.Machine.MachineLogs:input_type -> api.LogsRequest + 2, // 32: api.Machine.CheckPrerequisites:output_type -> api.CheckPrerequisitesResponse + 4, // 33: api.Machine.InitCluster:output_type -> api.InitClusterResponse + 24, // 34: api.Machine.JoinCluster:output_type -> google.protobuf.Empty + 8, // 35: api.Machine.Token:output_type -> api.TokenResponse + 0, // 36: api.Machine.Inspect:output_type -> api.MachineInfo + 6, // 37: api.Machine.InspectMachine:output_type -> api.InspectMachineResponse + 13, // 38: api.Machine.InspectWireGuardNetwork:output_type -> api.InspectWireGuardNetworkResponse + 24, // 39: api.Machine.Reset:output_type -> google.protobuf.Empty + 12, // 40: api.Machine.InspectService:output_type -> api.InspectServiceResponse + 26, // 41: api.Machine.MachineLogs:output_type -> api.LogEntry + 32, // [32:42] is the sub-list for method output_type + 22, // [22:32] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_internal_machine_api_pb_machine_proto_init() } diff --git a/internal/machine/api/pb/machine.proto b/internal/machine/api/pb/machine.proto index e4346bfc..e16b02eb 100644 --- a/internal/machine/api/pb/machine.proto +++ b/internal/machine/api/pb/machine.proto @@ -4,6 +4,7 @@ package api; option go_package = "github.com/psviderski/uncloud/internal/machine/api/pb"; +import "google/protobuf/duration.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "internal/machine/api/pb/common.proto"; @@ -133,6 +134,6 @@ message WireGuardPeer { } message RTTStats { - double median = 1; - double std_dev = 2; + google.protobuf.Duration median = 1; + google.protobuf.Duration std_dev = 2; } diff --git a/internal/machine/cluster/cluster.go b/internal/machine/cluster/cluster.go index f7782d26..d3c2b87c 100644 --- a/internal/machine/cluster/cluster.go +++ b/internal/machine/cluster/cluster.go @@ -345,7 +345,7 @@ func (c *Cluster) RemoveMachine(ctx context.Context, req *pb.RemoveMachineReques return &emptypb.Empty{}, nil } -// MemberRTTs returns the average and standard deviation of round-trip times from this member to each cluster member. +// MemberRTTs returns the median and standard deviation of round-trip times from this member to each cluster member. func (c *Cluster) MemberRTTs() ([]corrosion.MemberRTTStats, error) { return c.corroAdmin.ClusterMemberRTTs() } diff --git a/internal/machine/machine.go b/internal/machine/machine.go index 90d789c7..f10a28f6 100644 --- a/internal/machine/machine.go +++ b/internal/machine/machine.go @@ -42,6 +42,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -965,8 +966,8 @@ func (m *Machine) getMachineRTTs(ctx context.Context) (map[string]*pb.RTTStats, // Corrosion uses the management IP for gossip. if mid, ok := ipToMachineID[stats.Addr.Addr()]; ok { pbRTTs[mid] = &pb.RTTStats{ - Median: stats.Median, - StdDev: stats.StdDev, + Median: durationpb.New(stats.Median), + StdDev: durationpb.New(stats.StdDev), } } }