mirror of
https://github.com/karust/openserp.git
synced 2026-08-11 11:31:16 +08:00
174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTestCircuitBreaker(t *testing.T, cfg CircuitBreakerConfig) *CircuitBreaker {
|
|
t.Helper()
|
|
return NewCircuitBreaker("test-engine", cfg)
|
|
}
|
|
|
|
// TestCircuitBreaker_OpensAfterThreshold verifies that consecutive failures in closed state
|
|
// move the breaker to open exactly on configured threshold and block new requests.
|
|
func TestCircuitBreaker_OpensAfterThreshold(t *testing.T) {
|
|
cfg := CircuitBreakerConfig{
|
|
FailureThreshold: 3,
|
|
RecoveryTimeout: time.Second,
|
|
SuccessThreshold: 1,
|
|
}
|
|
cb := newTestCircuitBreaker(t, cfg)
|
|
|
|
cb.RecordFailure(context.Background())
|
|
cb.RecordFailure(context.Background())
|
|
if cb.State() != CircuitClosed {
|
|
t.Fatalf("expected closed after 2 failures, got: %s", cb.State())
|
|
}
|
|
|
|
cb.RecordFailure(context.Background())
|
|
if cb.State() != CircuitOpen {
|
|
t.Fatalf("expected open after %d failures, got: %s", cfg.FailureThreshold, cb.State())
|
|
}
|
|
if cb.AllowRequest(context.Background()) {
|
|
t.Error("expected request blocked in open state")
|
|
}
|
|
}
|
|
|
|
// TestCircuitBreaker_RecoveryToHalfOpen verifies timed recovery from open to half-open
|
|
// when recovery timeout elapses and a new request is attempted.
|
|
func TestCircuitBreaker_RecoveryToHalfOpen(t *testing.T) {
|
|
cfg := CircuitBreakerConfig{
|
|
FailureThreshold: 2,
|
|
RecoveryTimeout: 50 * time.Millisecond,
|
|
SuccessThreshold: 1,
|
|
}
|
|
cb := newTestCircuitBreaker(t, cfg)
|
|
|
|
cb.RecordFailure(context.Background())
|
|
cb.RecordFailure(context.Background())
|
|
if cb.State() != CircuitOpen {
|
|
t.Fatal("expected open")
|
|
}
|
|
|
|
time.Sleep(60 * time.Millisecond)
|
|
if !cb.AllowRequest(context.Background()) {
|
|
t.Error("should allow request after recovery timeout")
|
|
}
|
|
if cb.State() != CircuitHalfOpen {
|
|
t.Errorf("expected half-open, got: %s", cb.State())
|
|
}
|
|
}
|
|
|
|
// TestCircuitBreaker_HalfOpenSuccessClosesCircuit verifies that half-open state closes
|
|
// only after configured number of successful probes.
|
|
func TestCircuitBreaker_HalfOpenSuccessClosesCircuit(t *testing.T) {
|
|
cfg := CircuitBreakerConfig{
|
|
FailureThreshold: 1,
|
|
RecoveryTimeout: 20 * time.Millisecond,
|
|
SuccessThreshold: 2,
|
|
}
|
|
cb := newTestCircuitBreaker(t, cfg)
|
|
|
|
cb.RecordFailure(context.Background())
|
|
if cb.State() != CircuitOpen {
|
|
t.Fatalf("expected open, got: %s", cb.State())
|
|
}
|
|
|
|
time.Sleep(30 * time.Millisecond)
|
|
if !cb.AllowRequest(context.Background()) {
|
|
t.Fatal("expected request to pass in recovery window")
|
|
}
|
|
if cb.State() != CircuitHalfOpen {
|
|
t.Fatalf("expected half-open after recovery timeout, got: %s", cb.State())
|
|
}
|
|
|
|
cb.RecordSuccess(context.Background())
|
|
if cb.State() != CircuitHalfOpen {
|
|
t.Fatalf("expected to stay half-open until success threshold reached, got: %s", cb.State())
|
|
}
|
|
|
|
cb.RecordSuccess(context.Background())
|
|
if cb.State() != CircuitClosed {
|
|
t.Fatalf("expected closed after success threshold reached, got: %s", cb.State())
|
|
}
|
|
}
|
|
|
|
// TestCircuitBreaker_HalfOpenFailureReopens verifies that a failed probe in half-open
|
|
// immediately re-opens the circuit.
|
|
func TestCircuitBreaker_HalfOpenFailureReopens(t *testing.T) {
|
|
cfg := CircuitBreakerConfig{
|
|
FailureThreshold: 1,
|
|
RecoveryTimeout: 20 * time.Millisecond,
|
|
SuccessThreshold: 1,
|
|
}
|
|
cb := newTestCircuitBreaker(t, cfg)
|
|
|
|
cb.RecordFailure(context.Background())
|
|
time.Sleep(30 * time.Millisecond)
|
|
if !cb.AllowRequest(context.Background()) {
|
|
t.Fatal("expected probe request in half-open")
|
|
}
|
|
if cb.State() != CircuitHalfOpen {
|
|
t.Fatalf("expected half-open, got: %s", cb.State())
|
|
}
|
|
|
|
cb.RecordFailure(context.Background())
|
|
if cb.State() != CircuitOpen {
|
|
t.Fatalf("expected open after failed half-open probe, got: %s", cb.State())
|
|
}
|
|
}
|
|
|
|
// TestCircuitBreaker_Stats verifies stats payload fields and that retry_in is exposed
|
|
// only when breaker is open.
|
|
func TestCircuitBreaker_Stats(t *testing.T) {
|
|
cb := NewCircuitBreaker("test-engine", DefaultCircuitBreakerConfig())
|
|
cb.RecordFailure(context.Background())
|
|
|
|
stats := cb.Stats()
|
|
if stats["engine"] != "test-engine" {
|
|
t.Fatalf("expected engine=test-engine, got: %v", stats["engine"])
|
|
}
|
|
if stats["state"] != "closed" {
|
|
t.Fatalf("expected state=closed, got: %v", stats["state"])
|
|
}
|
|
if stats["failure_count"].(int) != 1 {
|
|
t.Fatalf("expected failure_count=1, got: %v", stats["failure_count"])
|
|
}
|
|
if _, ok := stats["retry_in"]; ok {
|
|
t.Fatalf("did not expect retry_in in closed state, got: %v", stats["retry_in"])
|
|
}
|
|
latencyCB := NewCircuitBreaker("latency-engine", DefaultCircuitBreakerConfig())
|
|
latencyCB.RecordSuccessDuration(context.Background(), 25*time.Millisecond)
|
|
latencyStats := latencyCB.Stats()
|
|
avg, ok := latencyStats["avg_response_ms"].(int64)
|
|
if !ok || avg <= 0 {
|
|
t.Fatalf("expected avg_response_ms int64 > 0, got: %v (%T)", latencyStats["avg_response_ms"], latencyStats["avg_response_ms"])
|
|
}
|
|
|
|
openCfg := CircuitBreakerConfig{FailureThreshold: 1, RecoveryTimeout: time.Second, SuccessThreshold: 1}
|
|
openCB := NewCircuitBreaker("open-engine", openCfg)
|
|
openCB.RecordFailure(context.Background())
|
|
openStats := openCB.Stats()
|
|
retryIn, ok := openStats["retry_in"].(int64)
|
|
if !ok {
|
|
t.Fatalf("expected retry_in int64 in open state, got: %T", openStats["retry_in"])
|
|
}
|
|
if retryIn <= 0 {
|
|
t.Fatalf("expected retry_in > 0 in open state, got: %d", retryIn)
|
|
}
|
|
}
|
|
|
|
// TestCircuitBreakerManager_AllStats verifies manager creates and reports per-engine breakers.
|
|
func TestCircuitBreakerManager_AllStats(t *testing.T) {
|
|
mgr := NewCircuitBreakerManager(DefaultCircuitBreakerConfig())
|
|
mgr.Get("google")
|
|
mgr.Get("yandex")
|
|
|
|
stats := mgr.AllStats()
|
|
if len(stats) != 2 {
|
|
t.Errorf("expected 2 entries, got: %d", len(stats))
|
|
}
|
|
}
|