feat: add initial code

This commit is contained in:
2025-07-27 23:35:04 +02:00
commit be73abca63
9 changed files with 871 additions and 0 deletions

View 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)
}
}