Files
MyClub/pkg/circuitbreaker/breaker.go
T
Tomas Dvorak b9cea0cd77 dev day #79
2025-11-02 01:04:02 +01:00

140 lines
2.9 KiB
Go

package circuitbreaker
import (
"errors"
"sync"
"time"
)
var (
// ErrCircuitOpen is returned when the circuit breaker is open
ErrCircuitOpen = errors.New("circuit breaker is open")
)
// State represents the circuit breaker state
type State int
const (
StateClosed State = iota
StateOpen
StateHalfOpen
)
// CircuitBreaker implements the circuit breaker pattern for external service calls
type CircuitBreaker struct {
mu sync.RWMutex
state State
failureCount int
successCount int
lastFailureTime time.Time
lastSuccessTime time.Time
maxFailures int
timeout time.Duration
halfOpenAttempts int
}
// New creates a new circuit breaker
// maxFailures: number of failures before opening the circuit
// timeout: how long to wait before attempting to close the circuit
func New(maxFailures int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
maxFailures: maxFailures,
timeout: timeout,
halfOpenAttempts: 3, // Allow 3 requests in half-open state
}
}
// Call executes the function if the circuit is closed
func (cb *CircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
// Check if we should transition from open to half-open
if cb.state == StateOpen && time.Since(cb.lastFailureTime) > cb.timeout {
cb.state = StateHalfOpen
cb.successCount = 0
}
// Reject if circuit is open
if cb.state == StateOpen {
cb.mu.Unlock()
return ErrCircuitOpen
}
// Allow limited attempts in half-open state
if cb.state == StateHalfOpen && cb.successCount >= cb.halfOpenAttempts {
cb.mu.Unlock()
return ErrCircuitOpen
}
cb.mu.Unlock()
// Execute the function
err := fn()
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.onFailure()
return err
}
cb.onSuccess()
return nil
}
// onFailure is called when a request fails
func (cb *CircuitBreaker) onFailure() {
cb.failureCount++
cb.lastFailureTime = time.Now()
if cb.state == StateHalfOpen {
// Immediately open if failure in half-open state
cb.state = StateOpen
cb.successCount = 0
return
}
if cb.failureCount >= cb.maxFailures {
cb.state = StateOpen
}
}
// onSuccess is called when a request succeeds
func (cb *CircuitBreaker) onSuccess() {
cb.lastSuccessTime = time.Now()
if cb.state == StateHalfOpen {
cb.successCount++
if cb.successCount >= cb.halfOpenAttempts {
// Close the circuit after successful attempts
cb.state = StateClosed
cb.failureCount = 0
cb.successCount = 0
}
return
}
if cb.state == StateClosed {
// Reset failure count on success
cb.failureCount = 0
}
}
// GetState returns the current state of the circuit breaker
func (cb *CircuitBreaker) GetState() State {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.state
}
// Reset manually resets the circuit breaker to closed state
func (cb *CircuitBreaker) Reset() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.state = StateClosed
cb.failureCount = 0
cb.successCount = 0
}