go-xmpp/client_manager.go
2019-06-08 11:15:51 +02:00

122 lines
3.3 KiB
Go

package xmpp // import "gosrc.io/xmpp"
import (
"time"
"golang.org/x/xerrors"
)
type PostConnect func(c *Client)
// ClientManager supervises an XMPP client connection. Its role is to handle connection events and
// apply reconnection strategy.
type ClientManager struct {
Client *Client
Session *Session
PostConnect PostConnect
// Store low level metrics
Metrics *Metrics
}
// NewClientManager creates a new client manager structure, intended to support
// handling XMPP client state event changes and auto-trigger reconnection
// based on ClientManager configuration.
func NewClientManager(client *Client, pc PostConnect) *ClientManager {
return &ClientManager{
Client: client,
PostConnect: pc,
}
}
// Start launch the connection loop
func (cm *ClientManager) Start() error {
cm.Client.Handler = func(e Event) {
switch e.State {
case StateConnected:
cm.Metrics.setConnectTime()
case StateSessionEstablished:
cm.Metrics.setLoginTime()
case StateDisconnected:
// Reconnect on disconnection
cm.connect()
case StateStreamError:
cm.Client.Disconnect()
// Only try reconnecting if we have not been kicked by another session to avoid connection loop.
if e.StreamError != "conflict" {
cm.connect()
}
}
}
return cm.connect()
}
// Stop cancels pending operations and terminates existing XMPP client.
func (cm *ClientManager) Stop() {
// Remove on disconnect handler to avoid triggering reconnect
cm.Client.Handler = nil
cm.Client.Disconnect()
}
// connect manages the reconnection loop and apply the define backoff to avoid overloading the server.
func (cm *ClientManager) connect() error {
var backoff Backoff // TODO: Group backoff calculation features with connection manager?
for {
var err error
// TODO: Make it possible to define logger to log disconnect and reconnection attempts
cm.Metrics = initMetrics()
if cm.Client.Session, err = cm.Client.Connect(); err != nil {
var actualErr ConnError
if xerrors.As(err, &actualErr) {
if actualErr.Permanent {
return xerrors.Errorf("unrecoverable connect error %w", actualErr)
}
}
backoff.Wait()
} else { // We are connected, we can leave the retry loop
break
}
}
if cm.PostConnect != nil {
cm.PostConnect(cm.Client)
}
return nil
}
// Client Metrics
// ============================================================================
type Metrics struct {
startTime time.Time
// ConnectTime returns the duration between client initiation of the TCP/IP
// connection to the server and actual TCP/IP session establishment.
// This time includes DNS resolution and can be slightly higher if the DNS
// resolution result was not in cache.
ConnectTime time.Duration
// LoginTime returns the between client initiation of the TCP/IP
// connection to the server and the return of the login result.
// This includes ConnectTime, but also XMPP level protocol negociation
// like starttls.
LoginTime time.Duration
}
// initMetrics set metrics with default value and define the starting point
// for duration calculation (connect time, login time, etc).
func initMetrics() *Metrics {
return &Metrics{
startTime: time.Now(),
}
}
func (m *Metrics) setConnectTime() {
m.ConnectTime = time.Since(m.startTime)
}
func (m *Metrics) setLoginTime() {
m.LoginTime = time.Since(m.startTime)
}