Delete inline comments that restate what the next line of code does (AX Principle 2). Affected: circuit_breaker.go, service.go, transport.go, worker.go, identity.go, peer.go, bufpool.go, settings_manager.go, node_service.go. Co-Authored-By: Charon <charon@lethean.io>
238 lines
6.8 KiB
Go
238 lines
6.8 KiB
Go
package mining
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"forge.lthn.ai/Snider/Mining/pkg/logging"
|
|
)
|
|
|
|
// if cb.State() == CircuitClosed { /* requests are flowing normally */ }
|
|
// if cb.State() == CircuitOpen { /* circuit tripped; requests are rejected */ }
|
|
// if cb.State() == CircuitHalfOpen { /* probe request allowed; awaiting SuccessThreshold */ }
|
|
type CircuitState int
|
|
|
|
const (
|
|
CircuitClosed CircuitState = iota
|
|
CircuitOpen
|
|
CircuitHalfOpen
|
|
)
|
|
|
|
func (s CircuitState) String() string {
|
|
switch s {
|
|
case CircuitClosed:
|
|
return "closed"
|
|
case CircuitOpen:
|
|
return "open"
|
|
case CircuitHalfOpen:
|
|
return "half-open"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// CircuitBreakerConfig{FailureThreshold: 3, ResetTimeout: 30 * time.Second, SuccessThreshold: 1}
|
|
type CircuitBreakerConfig struct {
|
|
FailureThreshold int
|
|
ResetTimeout time.Duration
|
|
SuccessThreshold int
|
|
}
|
|
|
|
// configuration := DefaultCircuitBreakerConfig()
|
|
// cb := NewCircuitBreaker("github-api", configuration)
|
|
func DefaultCircuitBreakerConfig() CircuitBreakerConfig {
|
|
return CircuitBreakerConfig{
|
|
FailureThreshold: 3,
|
|
ResetTimeout: 30 * time.Second,
|
|
SuccessThreshold: 1,
|
|
}
|
|
}
|
|
|
|
// cb := NewCircuitBreaker("github-api", DefaultCircuitBreakerConfig())
|
|
// result, err := cb.Execute(func() (interface{}, error) { return fetchStats(ctx) })
|
|
type CircuitBreaker struct {
|
|
name string
|
|
config CircuitBreakerConfig
|
|
state CircuitState
|
|
failures int
|
|
successes int
|
|
lastFailure time.Time
|
|
mutex sync.RWMutex
|
|
cachedResult interface{}
|
|
cachedErr error
|
|
lastCacheTime time.Time
|
|
cacheDuration time.Duration
|
|
}
|
|
|
|
// if err == ErrCircuitOpen { /* fallback to cached result */ }
|
|
var ErrCircuitOpen = NewMiningError(ErrCodeServiceUnavailable, "circuit breaker is open")
|
|
|
|
// cb := NewCircuitBreaker("github-api", DefaultCircuitBreakerConfig())
|
|
func NewCircuitBreaker(name string, config CircuitBreakerConfig) *CircuitBreaker {
|
|
return &CircuitBreaker{
|
|
name: name,
|
|
config: config,
|
|
state: CircuitClosed,
|
|
cacheDuration: 5 * time.Minute, // Cache successful results for 5 minutes
|
|
}
|
|
}
|
|
|
|
// if circuitBreaker.State() == CircuitOpen { return nil, ErrCircuitOpen }
|
|
func (circuitBreaker *CircuitBreaker) State() CircuitState {
|
|
circuitBreaker.mutex.RLock()
|
|
defer circuitBreaker.mutex.RUnlock()
|
|
return circuitBreaker.state
|
|
}
|
|
|
|
// result, err := circuitBreaker.Execute(func() (interface{}, error) { return fetchStats(ctx) })
|
|
func (circuitBreaker *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) {
|
|
if !circuitBreaker.allowRequest() {
|
|
circuitBreaker.mutex.RLock()
|
|
if circuitBreaker.cachedResult != nil && time.Since(circuitBreaker.lastCacheTime) < circuitBreaker.cacheDuration {
|
|
result := circuitBreaker.cachedResult
|
|
circuitBreaker.mutex.RUnlock()
|
|
logging.Debug("circuit breaker returning cached result", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
"state": circuitBreaker.state.String(),
|
|
})
|
|
return result, nil
|
|
}
|
|
circuitBreaker.mutex.RUnlock()
|
|
return nil, ErrCircuitOpen
|
|
}
|
|
|
|
result, err := fn()
|
|
|
|
if err != nil {
|
|
circuitBreaker.recordFailure()
|
|
} else {
|
|
circuitBreaker.recordSuccess(result)
|
|
}
|
|
|
|
return result, err
|
|
}
|
|
|
|
// if circuitBreaker.allowRequest() { /* execute the function */ }
|
|
func (circuitBreaker *CircuitBreaker) allowRequest() bool {
|
|
circuitBreaker.mutex.Lock()
|
|
defer circuitBreaker.mutex.Unlock()
|
|
|
|
switch circuitBreaker.state {
|
|
case CircuitClosed:
|
|
return true
|
|
|
|
case CircuitOpen:
|
|
if time.Since(circuitBreaker.lastFailure) > circuitBreaker.config.ResetTimeout {
|
|
circuitBreaker.state = CircuitHalfOpen
|
|
circuitBreaker.successes = 0
|
|
logging.Info("circuit breaker transitioning to half-open", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
})
|
|
return true
|
|
}
|
|
return false
|
|
|
|
case CircuitHalfOpen:
|
|
// Allow probe requests through
|
|
return true
|
|
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// circuitBreaker.recordFailure() // increments failures; opens circuit after FailureThreshold is reached
|
|
func (circuitBreaker *CircuitBreaker) recordFailure() {
|
|
circuitBreaker.mutex.Lock()
|
|
defer circuitBreaker.mutex.Unlock()
|
|
|
|
circuitBreaker.failures++
|
|
circuitBreaker.lastFailure = time.Now()
|
|
|
|
switch circuitBreaker.state {
|
|
case CircuitClosed:
|
|
if circuitBreaker.failures >= circuitBreaker.config.FailureThreshold {
|
|
circuitBreaker.state = CircuitOpen
|
|
logging.Warn("circuit breaker opened", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
"failures": circuitBreaker.failures,
|
|
})
|
|
}
|
|
|
|
case CircuitHalfOpen:
|
|
// Probe failed, back to open
|
|
circuitBreaker.state = CircuitOpen
|
|
logging.Warn("circuit breaker probe failed, reopening", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
})
|
|
}
|
|
}
|
|
|
|
// circuitBreaker.recordSuccess(stats) // caches result, resets failures; in HalfOpen closes the circuit after SuccessThreshold
|
|
func (circuitBreaker *CircuitBreaker) recordSuccess(result interface{}) {
|
|
circuitBreaker.mutex.Lock()
|
|
defer circuitBreaker.mutex.Unlock()
|
|
|
|
circuitBreaker.cachedResult = result
|
|
circuitBreaker.lastCacheTime = time.Now()
|
|
circuitBreaker.cachedErr = nil
|
|
|
|
switch circuitBreaker.state {
|
|
case CircuitClosed:
|
|
// Reset failure count on success
|
|
circuitBreaker.failures = 0
|
|
|
|
case CircuitHalfOpen:
|
|
circuitBreaker.successes++
|
|
if circuitBreaker.successes >= circuitBreaker.config.SuccessThreshold {
|
|
circuitBreaker.state = CircuitClosed
|
|
circuitBreaker.failures = 0
|
|
logging.Info("circuit breaker closed after successful probe", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// circuitBreaker.Reset() // force closed state after maintenance window
|
|
func (circuitBreaker *CircuitBreaker) Reset() {
|
|
circuitBreaker.mutex.Lock()
|
|
defer circuitBreaker.mutex.Unlock()
|
|
|
|
circuitBreaker.state = CircuitClosed
|
|
circuitBreaker.failures = 0
|
|
circuitBreaker.successes = 0
|
|
logging.Debug("circuit breaker manually reset", logging.Fields{
|
|
"name": circuitBreaker.name,
|
|
})
|
|
}
|
|
|
|
// if result, ok := circuitBreaker.GetCached(); ok { return result, nil }
|
|
func (circuitBreaker *CircuitBreaker) GetCached() (interface{}, bool) {
|
|
circuitBreaker.mutex.RLock()
|
|
defer circuitBreaker.mutex.RUnlock()
|
|
|
|
if circuitBreaker.cachedResult != nil && time.Since(circuitBreaker.lastCacheTime) < circuitBreaker.cacheDuration {
|
|
return circuitBreaker.cachedResult, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Global circuit breaker for GitHub API
|
|
var (
|
|
githubCircuitBreaker *CircuitBreaker
|
|
githubCircuitBreakerOnce sync.Once
|
|
)
|
|
|
|
// cb := getGitHubCircuitBreaker()
|
|
// result, err := cb.Execute(func() (interface{}, error) { return fetchLatestVersion(ctx) })
|
|
func getGitHubCircuitBreaker() *CircuitBreaker {
|
|
githubCircuitBreakerOnce.Do(func() {
|
|
githubCircuitBreaker = NewCircuitBreaker("github-api", CircuitBreakerConfig{
|
|
FailureThreshold: 3,
|
|
ResetTimeout: 60 * time.Second, // Wait 1 minute before retrying
|
|
SuccessThreshold: 1,
|
|
})
|
|
})
|
|
return githubCircuitBreaker
|
|
}
|