Skip to content

Commit

Permalink
fix: refine error handling with multiple tries
Browse files Browse the repository at this point in the history
  • Loading branch information
rami3l committed Feb 8, 2021
1 parent 91bca36 commit 1371aa4
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 28 deletions.
4 changes: 1 addition & 3 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@ 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,
ConnType: connType,
}
}

// 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")
Expand Down
31 changes: 20 additions & 11 deletions lib/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
41 changes: 27 additions & 14 deletions lib/tcping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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 <Ctrl+C>")
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()
Expand Down
2 changes: 2 additions & 0 deletions lib/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"time"
)

// SprintDuration pretty prints a time.Duration.
func SprintDuration(
format string,
t time.Duration,
Expand All @@ -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))
}

0 comments on commit 1371aa4

Please sign in to comment.