3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-14 16:09:32 +01:00

fix a race condition in idle timeouts

squigz on freenode reported an issue where bots were responding to PING
on time, but were occasionally being timed out regardless. This was a race
condition: timeout was detected as idleTime >= it.quitTimeout, but if
the client responded promptly to its PING message and sent no further messages,
but the main loop subsequently slept for longer than expected (i.e., significantly
longer than quitTimeout), this condition would be met through no fault of the
client's.

The fix here is to explicitly track the last time the ping was sent, then test
!lastSeen.After(lastPinged) instead (making use of time.Time's monotonicity).
It is sufficient that the measurement of lastPinged happens-before the PING is sent.
This commit is contained in:
Shivaram Lingamneni 2017-12-02 20:14:28 -05:00
parent f5e7ec525d
commit ad1e00629b

View File

@ -38,7 +38,6 @@ type IdleTimer struct {
// mutable // mutable
client *Client client *Client
state TimerState
lastSeen time.Time lastSeen time.Time
} }
@ -49,7 +48,6 @@ func NewIdleTimer(client *Client) *IdleTimer {
idleTimeout: IdleTimeout, idleTimeout: IdleTimeout,
quitTimeout: QuitTimeout, quitTimeout: QuitTimeout,
client: client, client: client,
state: TimerUnregistered,
} }
return &it return &it
} }
@ -58,17 +56,18 @@ 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.state = TimerUnregistered
it.lastSeen = time.Now() it.lastSeen = time.Now()
it.Unlock() it.Unlock()
go it.mainLoop() go it.mainLoop()
} }
func (it *IdleTimer) mainLoop() { func (it *IdleTimer) mainLoop() {
state := TimerUnregistered
var lastPinged time.Time
for { for {
it.Lock() it.Lock()
client := it.client client := it.client
state := it.state
lastSeen := it.lastSeen lastSeen := it.lastSeen
it.Unlock() it.Unlock()
@ -76,7 +75,8 @@ func (it *IdleTimer) mainLoop() {
return return
} }
idleTime := time.Now().Sub(lastSeen) now := time.Now()
idleTime := now.Sub(lastSeen)
var nextSleep time.Duration var nextSleep time.Duration
if state == TimerUnregistered { if state == TimerUnregistered {
@ -87,8 +87,8 @@ func (it *IdleTimer) mainLoop() {
nextSleep = it.registerTimeout - idleTime nextSleep = it.registerTimeout - idleTime
} }
} else if state == TimerIdle { } else if state == TimerIdle {
if idleTime < it.quitTimeout { if lastSeen.After(lastPinged) {
// new ping came in after we transitioned to TimerIdle, // new pong came in after we transitioned to TimerIdle,
// transition back to active and process deadlines below // transition back to active and process deadlines below
state = TimerActive state = TimerActive
} else { } else {
@ -100,6 +100,7 @@ func (it *IdleTimer) mainLoop() {
nextSleep = it.idleTimeout - idleTime nextSleep = it.idleTimeout - idleTime
if nextSleep <= 0 { if nextSleep <= 0 {
state = TimerIdle state = TimerIdle
lastPinged = now
client.Ping() client.Ping()
// grant the client at least quitTimeout to respond // grant the client at least quitTimeout to respond
nextSleep = it.quitTimeout nextSleep = it.quitTimeout
@ -113,10 +114,6 @@ func (it *IdleTimer) mainLoop() {
return return
} }
it.Lock()
it.state = state
it.Unlock()
time.Sleep(nextSleep) time.Sleep(nextSleep)
} }
} }