From 1371aa4586261dee460c73230899d5cca52391ad Mon Sep 17 00:00:00 2001 From: rami3l Date: Mon, 8 Feb 2021 23:43:16 +0100 Subject: [PATCH] fix: refine error handling with multiple tries --- cmd/cmd.go | 4 +--- lib/socket.go | 4 ++++ lib/stats.go | 31 ++++++++++++++++++++----------- lib/tcping.go | 41 +++++++++++++++++++++++++++-------------- lib/utils.go | 2 ++ 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 584be99..ef4e146 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -40,9 +40,7 @@ func App() (app *cobra.Command) { SetTryInterval(tryInterval). SetTimeout(timeout). EnableOutput() - if _, err := client.Run(); err != nil { - return err - } + client.Run() } return diff --git a/lib/socket.go b/lib/socket.go index 1d404e1..772996d 100644 --- a/lib/socket.go +++ b/lib/socket.go @@ -5,11 +5,13 @@ import ( "net" ) +// Socket is a wrapper of net.Conn. type Socket struct { Conn *net.Conn ConnType string } +// NewSocket initiates a Socket in default settings. func NewSocket(connType string) *Socket { return &Socket{ Conn: nil, @@ -17,12 +19,14 @@ func NewSocket(connType string) *Socket { } } +// Connect tries to open the socket connection. func (s *Socket) Connect(host string, port int) (err error) { conn, err := net.Dial(s.ConnType, JoinHostPort(host, port)) s.Conn = &conn return } +// Close tries to close the socket connection. func (s *Socket) Close() (err error) { if s.Conn == nil { return errors.New("connection not established") diff --git a/lib/stats.go b/lib/stats.go index e8e2ffd..ca14229 100644 --- a/lib/stats.go +++ b/lib/stats.go @@ -5,64 +5,73 @@ import ( "time" ) +// Result is a record of a single run of the Tcping client. type Result struct { ResponseTime time.Duration RemoteAddr net.Addr + Error error } +// Stats is the collection of Tcping client run records (Results). type Stats struct { Results []Result } +// Count returns the total number of records. func (s Stats) Count() (c int) { return len(s.Results) } +// SuccCount returns the number of success connections. func (s Stats) SuccCount() (sc int) { for _, r := range s.Results { - if r.ResponseTime > 0 { + if r.Error == nil { sc++ } } return } +// FailCount returns the number of failed connections. func (s Stats) FailCount() (fc int) { for _, r := range s.Results { - if r.ResponseTime <= 0 { + if r.Error != nil { fc++ } } return } -func (s Stats) MaxTime() (mt time.Duration) { +// MaxTime returns the maximum connection time. +func (s Stats) MaxTime() (maxt time.Duration) { if s.Count() <= 0 { return } - mt = s.Results[0].ResponseTime + maxt = s.Results[0].ResponseTime for _, r := range s.Results[1:] { - if t := r.ResponseTime; t > mt { - mt = t + if t := r.ResponseTime; t > maxt { + maxt = t } } return } -func (s Stats) MinTime() (mt time.Duration) { +// MinTime returns the minimum connection time. +func (s Stats) MinTime() (mint time.Duration) { if s.Count() <= 0 { return } - mt = s.Results[0].ResponseTime + mint = s.Results[0].ResponseTime for _, r := range s.Results[1:] { - if t := r.ResponseTime; t < mt { - mt = t + if t := r.ResponseTime; t < mint { + mint = t } } return } -func (s Stats) AvgTime() (at time.Duration) { +// AvgTime returns the average connection time. +func (s Stats) AvgTime() (avgt time.Duration) { if s.Count() <= 0 { return } diff --git a/lib/tcping.go b/lib/tcping.go index c2547e7..efdd59a 100644 --- a/lib/tcping.go +++ b/lib/tcping.go @@ -12,6 +12,7 @@ import ( // Indicates a connection timeout. const TimedOut = -1 +// TcpingClient is a ping-like speed test client, but works under TCP. type TcpingClient struct { Host string Port int @@ -21,6 +22,7 @@ type TcpingClient struct { outputOn bool } +// NewTcpingClient initializes a TcpingClient in default settings. func NewTcpingClient(host string) *TcpingClient { return &TcpingClient{ Host: host, @@ -51,15 +53,18 @@ func (c *TcpingClient) SetTimeout(timeout time.Duration) *TcpingClient { return c } +// EnableOutput turns on the output of TcpingClient to stdout. func (c *TcpingClient) EnableOutput() *TcpingClient { c.outputOn = true return c } +// HostAndPort returns the "host:port" pair of this client. func (c TcpingClient) HostAndPort() string { return JoinHostPort(c.Host, c.Port) } +// RunOnce makes a single tcping test. func (c TcpingClient) RunOnce() (responseTime time.Duration, remoteAddr net.Addr, err error) { socket := NewSocket("tcp") if c.outputOn { @@ -78,26 +83,28 @@ func (c TcpingClient) RunOnce() (responseTime time.Duration, remoteAddr net.Addr go asyncConnect(done) select { + // Connection finished (or returned an error) before timeout. case <-done: responseTime = time.Since(t0) if err != nil { if c.outputOn { fmt.Printf(": %s\n", err) } + // The default response time on error should be -1. + responseTime = -1 return } remoteAddr = (*socket.Conn).RemoteAddr() - if c.outputOn { - fmt.Printf(" (%s)", remoteAddr) - } if c.outputOn { fmt.Printf( - ": time=%s\n", + " (%s): time=%s\n", + remoteAddr, SprintDuration("%.2f", responseTime, time.Millisecond), ) } return + // Connection timed out. case <-timer.C: responseTime = TimedOut if c.outputOn { @@ -110,7 +117,8 @@ func (c TcpingClient) RunOnce() (responseTime time.Duration, remoteAddr net.Addr } } -func (c TcpingClient) Run() (s Stats, err error) { +// Run makes several consequent tcping tests and analyzes the overall result. +func (c TcpingClient) Run() (s Stats) { // Handle SIGINT and SIGTERM signalNotifier := make(chan os.Signal, 5) signal.Notify(signalNotifier, os.Interrupt, syscall.SIGTERM) @@ -119,25 +127,30 @@ func (c TcpingClient) Run() (s Stats, err error) { Loop: for i := 0; i < c.tryCount; func() { time.Sleep(c.tryInterval); i++ }() { + // If we have received a signal, we need to break the loop early. select { case <-signalNotifier: - fmt.Println("\r- Ctrl+C") + fmt.Println("\r ") break Loop default: } + + // Otherwise, we launch the client with `c.RunOnce()`. + // Show the number of tries. if c.outputOn { fmt.Printf("%3d> ", i) } - if responseTime, remoteAddr, err := c.RunOnce(); err != nil { - return Stats{Results: results}, err - } else { - results = append(results, Result{ - ResponseTime: responseTime, - RemoteAddr: remoteAddr, - }) - } + // We discard all errors here ON PURPOSE: + // errors should not stop the looping. + responseTime, remoteAddr, err := c.RunOnce() + results = append(results, Result{ + ResponseTime: responseTime, + RemoteAddr: remoteAddr, + Error: err, + }) } + // Analyze and print the final result. s = Stats{Results: results} if c.outputOn { count := s.Count() diff --git a/lib/utils.go b/lib/utils.go index 47a6f3e..9ef9bf7 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -7,6 +7,7 @@ import ( "time" ) +// SprintDuration pretty prints a time.Duration. func SprintDuration( format string, t time.Duration, @@ -18,6 +19,7 @@ func SprintDuration( return fmt.Sprintf(format, unitCount) + unitName } +// JoinHostPort makes a "host:port" pair as string. func JoinHostPort(host string, port int) string { return net.JoinHostPort(host, strconv.Itoa(port)) }