Repository: nats-io/nats-server (go)

Issue:
Allow sorting Connz by RTT
It would be nice to be able to sort `Connz` results by their RTT descending, as filtering on larger numbers of connections makes it hard to discover connections that have a high RTT to the server.

Pull request:
[FIXED] Allow sorting by rtt for connz.
Signed-off-by: Derek Collison <derek@nats.io>

Resolves #4150 



Patch:
diff --git a/server/monitor.go b/server/monitor.go
index f2ca6a63..76cd6150 100644
--- a/server/monitor.go
+++ b/server/monitor.go
@@ -137,6 +137,9 @@ type ConnInfo struct {
 	NameTag        string         `json:"name_tag,omitempty"`
 	Tags           jwt.TagList    `json:"tags,omitempty"`
 	MQTTClient     string         `json:"mqtt_client,omitempty"` // This is the MQTT client id
+
+	// Internal
+	rtt int64 // For fast sorting
 }
 
 // TLSPeerCert contains basic information about a TLS peer certificate
@@ -190,9 +193,7 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
 
 	if opts != nil {
 		// If no sort option given or sort is by uptime, then sort by cid
-		if opts.Sort == _EMPTY_ {
-			sortOpt = ByCid
-		} else {
+		if opts.Sort != _EMPTY_ {
 			sortOpt = opts.Sort
 			if !sortOpt.IsValid() {
 				return nil, fmt.Errorf("invalid sorting option: %s", sortOpt)
@@ -498,6 +499,8 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
 		sort.Sort(sort.Reverse(byStop{pconns}))
 	case ByReason:
 		sort.Sort(byReason{pconns})
+	case ByRTT:
+		sort.Sort(sort.Reverse(byRTT{pconns}))
 	}
 
 	minoff := c.Offset
@@ -527,6 +530,10 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) {
 // Fills in the ConnInfo from the client.
 // client should be locked.
 func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) {
+	// For fast sort if required.
+	rtt := client.getRTT()
+	ci.rtt = int64(rtt)
+
 	ci.Cid = client.cid
 	ci.MQTTClient = client.getMQTTClientID()
 	ci.Kind = client.kindString()
@@ -535,7 +542,7 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
 	ci.LastActivity = client.last
 	ci.Uptime = myUptime(now.Sub(client.start))
 	ci.Idle = myUptime(now.Sub(client.last))
-	ci.RTT = client.getRTT().String()
+	ci.RTT = rtt.String()
 	ci.OutMsgs = client.outMsgs
 	ci.OutBytes = client.outBytes
 	ci.NumSubs = uint32(len(client.subs))
@@ -584,7 +591,7 @@ func (c *client) getRTT() time.Duration {
 	if c.rtt == 0 {
 		// If a real client, go ahead and send ping now to get a value
 		// for RTT. For tests and telnet, or if client is closing, etc skip.
-		if c.opts.Lang != "" {
+		if c.opts.Lang != _EMPTY_ {
 			c.sendRTTPingLocked()
 		}
 		return 0
diff --git a/server/monitor_sort_opts.go b/server/monitor_sort_opts.go
index 10258d26..58cc3900 100644
--- a/server/monitor_sort_opts.go
+++ b/server/monitor_sort_opts.go
@@ -45,7 +45,7 @@ const (
 	ByUptime   SortOpt = "uptime"     // By the amount of time connections exist
 	ByStop     SortOpt = "stop"       // By the stop time for a closed connection
 	ByReason   SortOpt = "reason"     // By the reason for a closed connection
-
+	ByRTT      SortOpt = "rtt"        // By the round trip time
 )
 
 // Individual sort options provide the Less for sort.Interface. Len and Swap are on cList.
@@ -139,10 +139,15 @@ func (l byReason) Less(i, j int) bool {
 	return l.ConnInfos[i].Reason < l.ConnInfos[j].Reason
 }
 
+// RTT - Default is descending
+type byRTT struct{ ConnInfos }
+
+func (l byRTT) Less(i, j int) bool { return l.ConnInfos[i].rtt < l.ConnInfos[j].rtt }
+
 // IsValid determines if a sort option is valid
 func (s SortOpt) IsValid() bool {
 	switch s {
-	case "", ByCid, ByStart, BySubs, ByPending, ByOutMsgs, ByInMsgs, ByOutBytes, ByInBytes, ByLast, ByIdle, ByUptime, ByStop, ByReason:
+	case _EMPTY_, ByCid, ByStart, BySubs, ByPending, ByOutMsgs, ByInMsgs, ByOutBytes, ByInBytes, ByLast, ByIdle, ByUptime, ByStop, ByReason, ByRTT:
 		return true
 	default:
 		return false


Test patch:
diff --git a/server/monitor_test.go b/server/monitor_test.go
index b963a0f3..0c04796f 100644
--- a/server/monitor_test.go
+++ b/server/monitor_test.go
@@ -1,4 +1,4 @@
-// Copyright 2013-2022 The NATS Authors
+// Copyright 2013-2023 The NATS Authors
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
 // You may obtain a copy of the License at
@@ -4686,3 +4686,51 @@ func TestMonitorConnzOperatorModeFilterByUser(t *testing.T) {
 		require_True(t, ci.AuthorizedUser == aUser)
 	}
 }
+
+func TestMonitorConnzSortByRTT(t *testing.T) {
+	s := runMonitorServer()
+	defer s.Shutdown()
+
+	for i := 0; i < 10; i++ {
+		nc, err := nats.Connect(s.ClientURL())
+		require_NoError(t, err)
+		defer nc.Close()
+	}
+
+	connz := pollConz(t, s, 1, _EMPTY_, &ConnzOptions{Sort: ByRTT})
+	require_True(t, connz.NumConns == 10)
+
+	var rtt int64
+	for _, ci := range connz.Conns {
+		if rtt == 0 {
+			rtt = ci.rtt
+		} else {
+			if ci.rtt > rtt {
+				t.Fatalf("RTT not in descending order: %v vs %v",
+					time.Duration(rtt), time.Duration(ci.rtt))
+			}
+			rtt = ci.rtt
+		}
+	}
+
+	// Make sure url works as well.
+	url := fmt.Sprintf("http://127.0.0.1:%d/connz?sort=rtt", s.MonitorAddr().Port)
+	connz = pollConz(t, s, 0, url, nil)
+	require_True(t, connz.NumConns == 10)
+
+	rtt = 0
+	for _, ci := range connz.Conns {
+		crttd, err := time.ParseDuration(ci.RTT)
+		require_NoError(t, err)
+		crtt := int64(crttd)
+		if rtt == 0 {
+			rtt = crtt
+		} else {
+			if crtt > rtt {
+				t.Fatalf("RTT not in descending order: %v vs %v",
+					time.Duration(rtt), time.Duration(crtt))
+			}
+			rtt = ci.rtt
+		}
+	}
+}