188 lines
4.0 KiB
Go
188 lines
4.0 KiB
Go
package websocket
|
|
|
|
import (
|
|
"log"
|
|
"math/rand"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ReconnectConfig holds configuration for reconnection attempts
|
|
type ReconnectConfig struct {
|
|
MaxAttempts int
|
|
InitialDelay time.Duration
|
|
MaxDelay time.Duration
|
|
Multiplier float64
|
|
JitterFactor float64
|
|
TimeoutPerTry time.Duration
|
|
}
|
|
|
|
// DefaultReconnectConfig returns default reconnection configuration
|
|
func DefaultReconnectConfig() *ReconnectConfig {
|
|
return &ReconnectConfig{
|
|
MaxAttempts: 5,
|
|
InitialDelay: time.Second,
|
|
MaxDelay: time.Minute,
|
|
Multiplier: 2.0,
|
|
JitterFactor: 0.1,
|
|
TimeoutPerTry: time.Second * 5,
|
|
}
|
|
}
|
|
|
|
// ReconnectManager handles reconnection logic
|
|
type ReconnectManager struct {
|
|
config *ReconnectConfig
|
|
attempts int
|
|
lastDelay time.Duration
|
|
mu sync.RWMutex
|
|
active bool
|
|
stopChan chan struct{}
|
|
stats *ReconnectStats
|
|
}
|
|
|
|
// ReconnectStats tracks reconnection statistics
|
|
type ReconnectStats struct {
|
|
TotalAttempts int
|
|
SuccessfulRetries int
|
|
FailedRetries int
|
|
LastAttemptTime time.Time
|
|
TotalDowntime time.Duration
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewReconnectStats() *ReconnectStats {
|
|
return &ReconnectStats{
|
|
LastAttemptTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (rs *ReconnectStats) RecordAttempt(success bool) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
|
|
rs.TotalAttempts++
|
|
if success {
|
|
rs.SuccessfulRetries++
|
|
} else {
|
|
rs.FailedRetries++
|
|
}
|
|
rs.LastAttemptTime = time.Now()
|
|
}
|
|
|
|
func (rs *ReconnectStats) UpdateDowntime(duration time.Duration) {
|
|
rs.mu.Lock()
|
|
defer rs.mu.Unlock()
|
|
rs.TotalDowntime += duration
|
|
}
|
|
|
|
func (rs *ReconnectStats) GetStats() map[string]interface{} {
|
|
rs.mu.RLock()
|
|
defer rs.mu.RUnlock()
|
|
|
|
return map[string]interface{}{
|
|
"total_attempts": rs.TotalAttempts,
|
|
"successful_retries": rs.SuccessfulRetries,
|
|
"failed_retries": rs.FailedRetries,
|
|
"last_attempt_time": rs.LastAttemptTime,
|
|
"total_downtime": rs.TotalDowntime.String(),
|
|
"success_rate": float64(rs.SuccessfulRetries) / float64(rs.TotalAttempts),
|
|
}
|
|
}
|
|
|
|
func NewReconnectManager(config *ReconnectConfig) *ReconnectManager {
|
|
if config == nil {
|
|
config = DefaultReconnectConfig()
|
|
}
|
|
return &ReconnectManager{
|
|
config: config,
|
|
stopChan: make(chan struct{}),
|
|
stats: NewReconnectStats(),
|
|
lastDelay: config.InitialDelay,
|
|
}
|
|
}
|
|
|
|
func (rm *ReconnectManager) Start(connectFunc func() error) {
|
|
rm.mu.Lock()
|
|
if rm.active {
|
|
rm.mu.Unlock()
|
|
return
|
|
}
|
|
rm.active = true
|
|
rm.mu.Unlock()
|
|
|
|
go rm.reconnectLoop(connectFunc)
|
|
}
|
|
|
|
func (rm *ReconnectManager) Stop() {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
if rm.active {
|
|
close(rm.stopChan)
|
|
rm.active = false
|
|
}
|
|
}
|
|
|
|
func (rm *ReconnectManager) reconnectLoop(connectFunc func() error) {
|
|
for {
|
|
select {
|
|
case <-rm.stopChan:
|
|
return
|
|
default:
|
|
if rm.attempts >= rm.config.MaxAttempts {
|
|
log.Printf("Max reconnection attempts (%d) reached", rm.config.MaxAttempts)
|
|
rm.stats.RecordAttempt(false)
|
|
return
|
|
}
|
|
|
|
rm.attempts++
|
|
startTime := time.Now()
|
|
|
|
err := connectFunc()
|
|
if err == nil {
|
|
log.Printf("Successfully reconnected after %d attempts", rm.attempts)
|
|
rm.stats.RecordAttempt(true)
|
|
rm.stats.UpdateDowntime(time.Since(startTime))
|
|
rm.reset()
|
|
return
|
|
}
|
|
|
|
rm.stats.RecordAttempt(false)
|
|
rm.stats.UpdateDowntime(time.Since(startTime))
|
|
log.Printf("Reconnection attempt %d failed: %v", rm.attempts, err)
|
|
|
|
delay := rm.calculateDelay()
|
|
time.Sleep(delay)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rm *ReconnectManager) calculateDelay() time.Duration {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
delay := rm.lastDelay
|
|
rm.lastDelay = time.Duration(float64(rm.lastDelay) * rm.config.Multiplier)
|
|
if rm.lastDelay > rm.config.MaxDelay {
|
|
rm.lastDelay = rm.config.MaxDelay
|
|
}
|
|
|
|
// Add jitter
|
|
jitter := time.Duration(float64(rm.lastDelay) * rm.config.JitterFactor)
|
|
delay += time.Duration(float64(jitter) * (2*rand.Float64() - 1))
|
|
|
|
return delay
|
|
}
|
|
|
|
func (rm *ReconnectManager) reset() {
|
|
rm.mu.Lock()
|
|
defer rm.mu.Unlock()
|
|
|
|
rm.attempts = 0
|
|
rm.lastDelay = rm.config.InitialDelay
|
|
}
|
|
|
|
func (rm *ReconnectManager) GetStats() map[string]interface{} {
|
|
return rm.stats.GetStats()
|
|
}
|