Borg/pkg/circuitbreaker/circuitbreaker_test.go
google-labs-jules[bot] 2ff65938ca feat: Circuit breaker for failing domains
Implement a circuit breaker for the website collector to prevent hammering domains that are consistently failing.

The circuit breaker has three states: CLOSED, OPEN, and HALF-OPEN. It tracks failures per-domain and will open the circuit after a configurable number of consecutive failures. After a cooldown period, the circuit will transition to HALF-OPEN and allow a limited number of test requests to check for recovery.

The following command-line flags have been added to the `collect website` command:
- `--no-circuit-breaker`: Disable the circuit breaker
- `--circuit-failures`: Number of failures to trip the circuit breaker
- `--circuit-cooldown`: Cooldown time for the circuit breaker
- `--circuit-success-threshold`: Number of successes to close the circuit breaker
- `--circuit-half-open-requests`: Number of test requests in the half-open state

The implementation also includes:
- A new `circuitbreaker` package with the core logic
- Integration into the `website` package with per-domain tracking
- Improved logging to include the domain name and state changes
- Integration tests to verify the circuit breaker's behavior

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
2026-02-02 00:59:38 +00:00

101 lines
2.3 KiB
Go

package circuitbreaker
import (
"errors"
"testing"
"time"
)
func TestCircuitBreaker(t *testing.T) {
settings := Settings{
FailureThreshold: 2,
SuccessThreshold: 2,
Cooldown: 100 * time.Millisecond,
HalfOpenRequests: 2,
}
cb := New("test.com", settings)
// Initially closed
_, err := cb.Execute(func() (interface{}, error) {
return "success", nil
})
if err != nil {
t.Errorf("Expected success, got %v", err)
}
// Trip the breaker
_, err = cb.Execute(func() (interface{}, error) {
return nil, errors.New("failure 1")
})
if err == nil {
t.Error("Expected failure, got nil")
}
_, err = cb.Execute(func() (interface{}, error) {
return nil, errors.New("failure 2")
})
if err == nil {
t.Error("Expected failure, got nil")
}
// Now open
_, err = cb.Execute(func() (interface{}, error) {
return "should not be called", nil
})
if err == nil || err.Error() != "circuit is open for: failure 2" {
t.Errorf("Expected open circuit error, got %v", err)
}
// Wait for cooldown
time.Sleep(150 * time.Millisecond)
// Half-open, should succeed
_, err = cb.Execute(func() (interface{}, error) {
return "success", nil
})
if err != nil {
t.Errorf("Expected success in half-open, got %v", err)
}
// Still half-open, need another success
_, err = cb.Execute(func() (interface{}, error) {
return "success", nil
})
if err != nil {
t.Errorf("Expected success in half-open, got %v", err)
}
// Now closed again
_, err = cb.Execute(func() (interface{}, error) {
return "success", nil
})
if err != nil {
t.Errorf("Expected success in closed state, got %v", err)
}
// Trip again to test half-open failure
cb.Execute(func() (interface{}, error) {
return nil, errors.New("failure 1")
})
cb.Execute(func() (interface{}, error) {
return nil, errors.New("failure 2")
})
time.Sleep(150 * time.Millisecond)
// Half-open, but fail
_, err = cb.Execute(func() (interface{}, error) {
return nil, errors.New("half-open failure")
})
if err == nil {
t.Error("Expected failure in half-open, got nil")
}
// Should be open again
_, err = cb.Execute(func() (interface{}, error) {
return "should not be called", nil
})
if err == nil || err.Error() != "circuit is open for: half-open failure" {
t.Errorf("Expected open circuit error, got %v", err)
}
}