Merge pull request #173 from slingamn/timeouts.3

refactor idle timeouts again
This commit is contained in:
Daniel Oaks 2017-12-11 14:53:05 +10:00 committed by GitHub
commit 36b26f99be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 59 additions and 74 deletions

View File

@ -26,6 +26,7 @@ const (
TimerUnregistered TimerState = iota // client is unregistered TimerUnregistered TimerState = iota // client is unregistered
TimerActive // client is actively sending commands TimerActive // client is actively sending commands
TimerIdle // client is idle, we sent PING and are waiting for PONG TimerIdle // client is idle, we sent PING and are waiting for PONG
TimerDead // client was terminated
) )
type IdleTimer struct { type IdleTimer struct {
@ -35,10 +36,11 @@ type IdleTimer struct {
registerTimeout time.Duration registerTimeout time.Duration
idleTimeout time.Duration idleTimeout time.Duration
quitTimeout time.Duration quitTimeout time.Duration
client *Client
// mutable // mutable
client *Client state TimerState
lastSeen time.Time timer *time.Timer
} }
// NewIdleTimer sets up a new IdleTimer using constant timeouts. // NewIdleTimer sets up a new IdleTimer using constant timeouts.
@ -56,91 +58,74 @@ func NewIdleTimer(client *Client) *IdleTimer {
// it will eventually be stopped. // it will eventually be stopped.
func (it *IdleTimer) Start() { func (it *IdleTimer) Start() {
it.Lock() it.Lock()
it.lastSeen = time.Now() defer it.Unlock()
it.Unlock() it.state = TimerUnregistered
go it.mainLoop() it.resetTimeout()
} }
func (it *IdleTimer) mainLoop() {
state := TimerUnregistered
var lastPinged time.Time
for {
it.Lock()
client := it.client
lastSeen := it.lastSeen
it.Unlock()
if client == nil {
return
}
now := time.Now()
idleTime := now.Sub(lastSeen)
var nextSleep time.Duration
if state == TimerUnregistered {
if client.Registered() {
// transition to active, process new deadlines below
state = TimerActive
} else {
nextSleep = it.registerTimeout - idleTime
}
} else if state == TimerIdle {
if lastSeen.After(lastPinged) {
// new pong came in after we transitioned to TimerIdle,
// transition back to active and process deadlines below
state = TimerActive
} else {
nextSleep = 0
}
}
if state == TimerActive {
nextSleep = it.idleTimeout - idleTime
if nextSleep <= 0 {
state = TimerIdle
lastPinged = now
client.Ping()
// grant the client at least quitTimeout to respond
nextSleep = it.quitTimeout
}
}
if nextSleep <= 0 {
// ran out of time, hang them up
client.Quit(it.quitMessage(state))
client.destroy()
return
}
time.Sleep(nextSleep)
}
}
// Touch registers activity (e.g., sending a command) from an client.
func (it *IdleTimer) Touch() { func (it *IdleTimer) Touch() {
it.Lock() // ignore touches from unregistered clients
client := it.client if !it.client.Registered() {
it.Unlock()
// ignore touches for unregistered clients
if client != nil && !client.Registered() {
return return
} }
it.Lock() it.Lock()
it.lastSeen = time.Now() defer it.Unlock()
it.Unlock() // a touch transitions TimerUnregistered or TimerIdle into TimerActive
if it.state != TimerDead {
it.state = TimerActive
it.resetTimeout()
}
}
func (it *IdleTimer) processTimeout() {
var previousState TimerState
func() {
it.Lock()
defer it.Unlock()
previousState = it.state
// TimerActive transitions to TimerIdle, all others to TimerDead
if it.state == TimerActive {
// send them a ping, give them time to respond
it.state = TimerIdle
it.resetTimeout()
} else {
it.state = TimerDead
}
}()
if previousState == TimerActive {
it.client.Ping()
} else {
it.client.Quit(it.quitMessage(previousState))
it.client.destroy()
}
} }
// Stop stops counting idle time. // Stop stops counting idle time.
func (it *IdleTimer) Stop() { func (it *IdleTimer) Stop() {
it.Lock() it.Lock()
defer it.Unlock() defer it.Unlock()
// no need to stop the goroutine, it'll clean itself up in a few minutes; it.state = TimerDead
// just ensure the Client object is collectable it.resetTimeout()
it.client = nil }
func (it *IdleTimer) resetTimeout() {
if it.timer != nil {
it.timer.Stop()
}
var nextTimeout time.Duration
switch it.state {
case TimerUnregistered:
nextTimeout = it.registerTimeout
case TimerActive:
nextTimeout = it.idleTimeout
case TimerIdle:
nextTimeout = it.quitTimeout
case TimerDead:
return
}
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
} }
func (it *IdleTimer) quitMessage(state TimerState) string { func (it *IdleTimer) quitMessage(state TimerState) string {