Fix issues/9 by implementing a client-side ping loop.

This commit is contained in:
Alex Bramley 2011-11-15 22:17:29 +00:00
parent 907560b599
commit 8fa5e5624e
2 changed files with 95 additions and 3 deletions

View File

@ -47,14 +47,17 @@ type Conn struct {
Connected bool Connected bool
// Control channels to goroutines // Control channels to goroutines
cSend, cLoop chan bool cSend, cLoop, cPing chan bool
// Misc knobs to tweak client behaviour: // Misc knobs to tweak client behaviour:
// Are we connecting via SSL? Do we care about certificate validity? // Are we connecting via SSL? Do we care about certificate validity?
SSL bool SSL bool
SSLConfig *tls.Config SSLConfig *tls.Config
// Socket timeout, in seconds. Defaulted to 5m in New(). // Client->server ping frequency, in seconds. Defaults to 3m.
PingFreq int64
// Socket timeout, in seconds. Default to 5m.
Timeout int64 Timeout int64
// Set this to true to disable flood protection and false to re-enable // Set this to true to disable flood protection and false to re-enable
@ -95,8 +98,10 @@ func Client(nick, ident, name string,
out: make(chan string, 32), out: make(chan string, 32),
cSend: make(chan bool), cSend: make(chan bool),
cLoop: make(chan bool), cLoop: make(chan bool),
cPing: make(chan bool),
SSL: false, SSL: false,
SSLConfig: nil, SSLConfig: nil,
PingFreq: 180,
Timeout: 300, Timeout: 300,
Flood: false, Flood: false,
badness: 0, badness: 0,
@ -194,6 +199,7 @@ func (conn *Conn) postConnect() {
conn.sock.SetTimeout(conn.Timeout * second) conn.sock.SetTimeout(conn.Timeout * second)
go conn.send() go conn.send()
go conn.recv() go conn.recv()
go conn.ping()
go conn.runLoop() go conn.runLoop()
} }
@ -236,6 +242,20 @@ func (conn *Conn) recv() {
} }
} }
// Repeatedly pings the server every PingFreq seconds (no matter what)
func (conn *Conn) ping() {
tick := time.NewTicker(conn.PingFreq * second)
for {
select {
case <-tick.C:
conn.Raw(fmt.Sprintf("PING :%d", time.Nanoseconds()))
case <-conn.cPing:
tick.Stop()
return
}
}
}
// goroutine to dispatch events for lines received on input channel // goroutine to dispatch events for lines received on input channel
func (conn *Conn) runLoop() { func (conn *Conn) runLoop() {
for { for {
@ -303,6 +323,7 @@ func (conn *Conn) shutdown() {
conn.sock.Close() conn.sock.Close()
conn.cSend <- true conn.cSend <- true
conn.cLoop <- true conn.cLoop <- true
conn.cPing <- true
// reinit datastructures ready for next connection // reinit datastructures ready for next connection
// do this here rather than after runLoop()'s for due to race // do this here rather than after runLoop()'s for due to race
conn.initialise() conn.initialise()

View File

@ -6,6 +6,7 @@ import (
"github.com/fluffle/golog/logging" "github.com/fluffle/golog/logging"
"github.com/fluffle/goirc/state" "github.com/fluffle/goirc/state"
"gomock.googlecode.com/hg/gomock" "gomock.googlecode.com/hg/gomock"
"strings"
"testing" "testing"
"time" "time"
) )
@ -260,6 +261,7 @@ func TestRecv(t *testing.T) {
// channels manually for recv() to be able to call shutdown correctly. // channels manually for recv() to be able to call shutdown correctly.
<-c.cSend <-c.cSend
<-c.cLoop <-c.cLoop
<-c.cPing
// Give things time to shake themselves out... // Give things time to shake themselves out...
<-time.After(1e6) <-time.After(1e6)
if !exited { if !exited {
@ -272,6 +274,74 @@ func TestRecv(t *testing.T) {
} }
} }
func TestPing(t *testing.T) {
// Passing a second value to setUp inhibits postConnect()
c, s := setUp(t, false)
// We can't use tearDown here, as it will cause a deadlock in shutdown()
// trying to send kill messages down channels to nonexistent goroutines.
defer s.ctrl.Finish()
// Set a low ping frequency for testing.
// This still increases testing time by a good few seconds :-/
c.PingFreq = 1
// reader is a helper to do a "non-blocking" read of c.out
reader := func() string {
select {
case <-time.After(1e6):
case s := <-c.out:
return s
}
return ""
}
if s := reader(); s != "" {
t.Errorf("Line output before ping started.")
}
// Start ping loop.
exited := false
go func() {
c.ping()
exited = true
}()
// The first ping should be after a second,
// so we don't expect anything now on c.in
if s := reader(); s != "" {
t.Errorf("Line output directly after ping started.")
}
<-time.After(1e9)
if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
t.Errorf("Line not output after 1 second.")
}
<-time.After(1e7)
if s := reader(); s != "" {
t.Errorf("Line output under a second after last ping.")
}
<-time.After(1e9)
if s := reader(); s == "" || !strings.HasPrefix(s, "PING :") {
t.Errorf("Line not output after another second.")
}
// Now kill the ping loop.
if exited {
t.Errorf("Exited before signal sent.")
}
c.cPing <- true
<-time.After(1e9)
if s := reader(); s != "" {
t.Errorf("Line output after ping stopped.")
}
if !exited {
t.Errorf("Didn't exit after signal.")
}
}
func TestRunLoop(t *testing.T) { func TestRunLoop(t *testing.T) {
// Passing a second value to setUp inhibits postConnect() // Passing a second value to setUp inhibits postConnect()
c, s := setUp(t, false) c, s := setUp(t, false)
@ -364,6 +434,7 @@ func TestWrite(t *testing.T) {
go func() { go func() {
<-c.cSend <-c.cSend
<-c.cLoop <-c.cLoop
<-c.cPing
}() }()
s.nc.Close() s.nc.Close()