feat: add initial code
This commit is contained in:
166
internal/quake3/client/robust.go
Normal file
166
internal/quake3/client/robust.go
Normal file
@ -0,0 +1,166 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user