3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00
ergo/irc/idletimer.go

158 lines
3.6 KiB
Go
Raw Normal View History

2017-10-15 18:24:28 +02:00
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"fmt"
"sync"
"time"
)
const (
// RegisterTimeout is how long clients have to register before we disconnect them
RegisterTimeout = time.Minute
// IdleTimeout is how long without traffic before a registered client is considered idle.
IdleTimeout = time.Minute + time.Second*30
// QuitTimeout is how long without traffic before an idle client is disconnected
QuitTimeout = time.Minute
)
2017-10-15 18:24:28 +02:00
// client idleness state machine
type TimerState uint
const (
TimerUnregistered TimerState = iota // client is unregistered
TimerActive // client is actively sending commands
TimerIdle // client is idle, we sent PING and are waiting for PONG
)
type IdleTimer struct {
2017-11-22 10:41:11 +01:00
sync.Mutex // tier 1
2017-10-15 18:24:28 +02:00
// immutable after construction
registerTimeout time.Duration
idleTimeout time.Duration
quitTimeout time.Duration
// mutable
client *Client
lastSeen time.Time
}
// NewIdleTimer sets up a new IdleTimer using constant timeouts.
func NewIdleTimer(client *Client) *IdleTimer {
it := IdleTimer{
registerTimeout: RegisterTimeout,
idleTimeout: IdleTimeout,
quitTimeout: QuitTimeout,
client: client,
}
return &it
}
// Start starts counting idle time; if there is no activity from the client,
// it will eventually be stopped.
func (it *IdleTimer) Start() {
it.Lock()
it.lastSeen = time.Now()
it.Unlock()
go it.mainLoop()
}
func (it *IdleTimer) mainLoop() {
state := TimerUnregistered
var lastPinged time.Time
2017-10-15 18:24:28 +02:00
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
2017-10-15 18:24:28 +02:00
if state == TimerUnregistered {
if client.Registered() {
// transition to active, process new deadlines below
state = TimerActive
} else {
nextSleep = it.registerTimeout - idleTime
2017-10-15 18:24:28 +02:00
}
} 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
2017-10-15 18:24:28 +02:00
}
}
if state == TimerActive {
2017-10-15 18:24:28 +02:00
nextSleep = it.idleTimeout - idleTime
if nextSleep <= 0 {
state = TimerIdle
lastPinged = now
client.Ping()
// grant the client at least quitTimeout to respond
nextSleep = it.quitTimeout
}
2017-10-15 18:24:28 +02:00
}
if nextSleep <= 0 {
// ran out of time, hang them up
client.Quit(it.quitMessage(state))
2017-10-15 18:24:28 +02:00
client.destroy()
return
}
time.Sleep(nextSleep)
}
}
// Touch registers activity (e.g., sending a command) from an client.
func (it *IdleTimer) Touch() {
it.Lock()
client := it.client
it.Unlock()
// ignore touches for unregistered clients
if client != nil && !client.Registered() {
return
}
it.Lock()
it.lastSeen = time.Now()
it.Unlock()
}
// Stop stops counting idle time.
func (it *IdleTimer) Stop() {
it.Lock()
defer it.Unlock()
// no need to stop the goroutine, it'll clean itself up in a few minutes;
// just ensure the Client object is collectable
it.client = nil
}
func (it *IdleTimer) quitMessage(state TimerState) string {
switch state {
case TimerUnregistered:
return fmt.Sprintf("Registration timeout: %v", it.registerTimeout)
case TimerIdle:
// how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout).
return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
default:
// shouldn't happen
return ""
}
}