Add heartbeat ACK response and error handling (#396)

* Add heartbeat ACK response and error handling

- Error when sending a heartbeat now triggers a reconnection
- Op7 now triggers a reconnection
- Session now tracks the last heartbeat ACK that was recieved. If the
last ACK is more than FailedHeartbeatAcks*heartbeatinterval in the past,
this is treated as a dead connection and a reconnection is forced.

* Address @iopred comments
This commit is contained in:
Erik McClure 2017-06-27 20:52:59 -07:00 committed by Chris Rhodes
parent 5a02430c02
commit bb4b96e26d
3 changed files with 33 additions and 8 deletions

View file

@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) {
MaxRestRetries: 3, MaxRestRetries: 3,
Client: &http.Client{Timeout: (20 * time.Second)}, Client: &http.Client{Timeout: (20 * time.Second)},
sequence: new(int64), sequence: new(int64),
LastHeartbeatAck: time.Now().UTC(),
} }
// If no arguments are passed return the empty Session interface. // If no arguments are passed return the empty Session interface.

View file

@ -78,6 +78,9 @@ type Session struct {
// The http client used for REST requests // The http client used for REST requests
Client *http.Client Client *http.Client
// Stores the last HeartbeatAck that was recieved (in UTC)
LastHeartbeatAck time.Time
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance

View file

@ -131,6 +131,7 @@ func (s *Session) Open() (err error) {
// lock. // lock.
s.listening = make(chan interface{}) s.listening = make(chan interface{})
go s.listen(s.wsConn, s.listening) go s.listen(s.wsConn, s.listening)
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock() s.Unlock()
@ -199,10 +200,13 @@ type helloOp struct {
Trace []string `json:"_trace"` Trace []string `json:"_trace"`
} }
// Number of heartbeat intervals to wait until forcing a connection restart.
const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond
// heartbeat sends regular heartbeats to Discord so it knows the client // heartbeat sends regular heartbeats to Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will // is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds. // disconnect the websocket connection after a few seconds.
func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) {
s.log(LogInformational, "called") s.log(LogInformational, "called")
@ -211,20 +215,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}
} }
var err error var err error
ticker := time.NewTicker(i * time.Millisecond) ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for { for {
s.RLock()
last := s.LastHeartbeatAck
s.RUnlock()
sequence := atomic.LoadInt64(s.sequence) sequence := atomic.LoadInt64(s.sequence)
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
s.wsMutex.Lock() s.wsMutex.Lock()
err = wsConn.WriteJSON(heartbeatOp{1, sequence}) err = wsConn.WriteJSON(heartbeatOp{1, sequence})
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) {
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err)
s.Lock() } else {
s.DataReady = false s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last))
s.Unlock() }
s.Close()
s.reconnect()
return return
} }
s.Lock() s.Lock()
@ -398,7 +408,10 @@ func (s *Session) onEvent(messageType int, message []byte) {
// Reconnect // Reconnect
// Must immediately disconnect from gateway and reconnect to new gateway. // Must immediately disconnect from gateway and reconnect to new gateway.
if e.Operation == 7 { if e.Operation == 7 {
// TODO s.log(LogInformational, "Closing and reconnecting in response to Op7")
s.Close()
s.reconnect()
return
} }
// Invalid Session // Invalid Session
@ -426,6 +439,14 @@ func (s *Session) onEvent(messageType int, message []byte) {
return return
} }
if e.Operation == 11 {
s.Lock()
s.LastHeartbeatAck = time.Now().UTC()
s.Unlock()
s.log(LogInformational, "got heartbeat ACK")
return
}
// Do not try to Dispatch a non-Dispatch Message // Do not try to Dispatch a non-Dispatch Message
if e.Operation != 0 { if e.Operation != 0 {
// But we probably should be doing something with them. // But we probably should be doing something with them.