Mining/pkg/mining/circuit_breaker.go
Claude 4e7d4d906b
ax(batch): remove redundant prose comments in source files
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>
2026-04-02 18:48:08 +01:00

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
}