167 lines
3.4 KiB
Go
167 lines
3.4 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
checkInterval = 2 * time.Second
|
|
minBackoff = 1 * time.Second
|
|
maxBackoff = 60 * time.Second
|
|
backoffFactor = 2.0
|
|
|
|
PongEvent = "PONG"
|
|
FailedPingEvent = "UNREACHABLE"
|
|
)
|
|
|
|
type Observer interface {
|
|
Update(event string)
|
|
GetID() string
|
|
}
|
|
|
|
type RobustQ3Client struct {
|
|
Client *Q3Client
|
|
backoff time.Duration
|
|
attempt uint
|
|
stop chan bool
|
|
observers map[string]Observer
|
|
}
|
|
|
|
func NewRobust(ctx context.Context, client *Q3Client) *RobustQ3Client {
|
|
c := &RobustQ3Client{
|
|
Client: client,
|
|
backoff: minBackoff,
|
|
attempt: 0,
|
|
stop: make(chan bool),
|
|
observers: make(map[string]Observer),
|
|
}
|
|
go c.keepAliveLoop(ctx)
|
|
return c
|
|
}
|
|
|
|
func (rc *RobustQ3Client) Stop() error {
|
|
close(rc.stop)
|
|
return rc.Client.Close()
|
|
}
|
|
|
|
func (rc *RobustQ3Client) Ping() error {
|
|
return rc.Client.Ping()
|
|
}
|
|
|
|
func (rc *RobustQ3Client) GetInfo() (map[string]string, error) {
|
|
return rc.Client.GetInfo()
|
|
}
|
|
|
|
func (rc *RobustQ3Client) GetStatus() (*Status, error) {
|
|
return rc.Client.GetStatus()
|
|
}
|
|
|
|
func (rc *RobustQ3Client) Subscribe(o Observer) {
|
|
rc.observers[o.GetID()] = o
|
|
}
|
|
|
|
func (rc *RobustQ3Client) Unsubscribe(id string) {
|
|
delete(rc.observers, id)
|
|
}
|
|
|
|
func (rc *RobustQ3Client) keepAliveLoop(ctx context.Context) {
|
|
slog.Info("Starting robust client reconnect loop", "interval", checkInterval)
|
|
|
|
ticker := time.NewTicker(checkInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
slog.Info("Exiting robust client reconnect loop", "reason", "context cancelled")
|
|
return
|
|
case <-rc.stop:
|
|
slog.Info("Exiting robust client reconnect loop", "reason", "stop signal")
|
|
return
|
|
case <-ticker.C:
|
|
slog.Debug("Tick; Trying to ping gameserver")
|
|
rc.tryPing(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rc *RobustQ3Client) tryPing(ctx context.Context) {
|
|
if err := rc.Client.Ping(); err != nil {
|
|
slog.Warn("No response to gameserver ping", "error", err)
|
|
rc.notifyObservers(FailedPingEvent)
|
|
rc.reconnectLoop(ctx)
|
|
} else {
|
|
slog.Debug("Pong!")
|
|
rc.notifyObservers(PongEvent)
|
|
}
|
|
}
|
|
|
|
func (rc *RobustQ3Client) reconnectLoop(ctx context.Context) {
|
|
slog.Debug("Waiting until gameserver is reachable")
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
slog.Info("Exiting reconnect attempt loop", "reason", "context cancelled")
|
|
return
|
|
case <-rc.stop:
|
|
slog.Info("Exiting reconnect attempt loop", "reason", "stop signal")
|
|
return
|
|
default:
|
|
if err := rc.attemptReconnect(ctx); err != nil {
|
|
rc.notifyObservers(FailedPingEvent)
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rc *RobustQ3Client) attemptReconnect(ctx context.Context) error {
|
|
if err := rc.Client.Ping(); err != nil {
|
|
slog.Warn(
|
|
"Failed to reach gameserver",
|
|
"error", err,
|
|
"backoff", rc.backoff,
|
|
"attempt", rc.attempt,
|
|
)
|
|
|
|
timer := time.NewTimer(rc.backoff)
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
case <-rc.stop:
|
|
timer.Stop()
|
|
case <-timer.C:
|
|
rc.incRetryVars()
|
|
}
|
|
return err
|
|
}
|
|
|
|
slog.Info("Successfully reached gameserver", "attempts", rc.attempt)
|
|
rc.resetRetryVars()
|
|
return nil
|
|
}
|
|
|
|
func (rc *RobustQ3Client) incRetryVars() {
|
|
rc.backoff = min(
|
|
time.Duration(float64(rc.backoff)*backoffFactor),
|
|
maxBackoff,
|
|
)
|
|
rc.attempt++
|
|
}
|
|
|
|
func (rc *RobustQ3Client) resetRetryVars() {
|
|
rc.backoff = minBackoff
|
|
rc.attempt = 0
|
|
}
|
|
|
|
func (rc *RobustQ3Client) notifyObservers(event string) {
|
|
slog.Debug("Notifying all observers", "event", event)
|
|
for _, o := range rc.observers {
|
|
o.Update(event)
|
|
}
|
|
}
|