package main import ( "errors" "time" "github.com/sony/gobreaker" "github.com/stretchr/testify/assert" ) // TestCircuitBreakerStateTransitions tests the circuit breaker state transitions: // Closed -> Open -> Half-Open -> Closed/Open func (suite *CircuitBreakerTestSuite) TestCircuitBreakerStateTransitions() { // Reset the buffer before the test suite.outputBuffer.Reset() // Initialize circuit breaker with a shorter timeout for testing cfg.CircuitBreaker.Timeout = 1 // 1 second timeout to half-open state cfg.CircuitBreaker.MaxFailures = 3 initCircuitBreaker(cfg) // 1. Initially the circuit should be closed assert.Equal(suite.T(), gobreaker.StateClosed.String(), cb.State().String(), "Circuit should start in closed state") // 2. Generate failures to trip the circuit testErr := errors.New("test error") for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ { _, err := cb.Execute(func() (any, error) { return nil, testErr }) assert.Error(suite.T(), err, "Execute should return error") } // 3. Circuit should now be open assert.Equal(suite.T(), gobreaker.StateOpen.String(), cb.State().String(), "Circuit should transition to open state after failures") // Verify that requests are rejected during open state _, err := cb.Execute(func() (any, error) { return "success", nil }) assert.Equal(suite.T(), gobreaker.ErrOpenState.Error(), err.Error(), "Should return ErrOpenState when circuit is open") // Verify that the state change was logged assert.True(suite.T(), suite.logContains("Circuit breaker state changed"), "State change should be logged") assert.True(suite.T(), suite.logContains(`"from":"closed"`), "Log should mention transition from closed state") assert.True(suite.T(), suite.logContains(`"to":"open"`), "Log should mention transition to open state") // 4. Wait for timeout to allow transition to half-open time.Sleep(time.Duration(cfg.CircuitBreaker.Timeout+1) * time.Second) // The next request should transition the circuit to half-open // (Sony's gobreaker transitions to half-open on the next request after timeout) tmpState := cb.State() // Execute a successful request to check state _, _ = cb.Execute(func() (any, error) { return "success", nil }) // 5. Verify half-open state was reached suite.T().Logf("Current circuit state: %s", cb.State()) if tmpState.String() != gobreaker.StateHalfOpen.String() { suite.T().Skip("Circuit didn't transition to half-open as expected, likely due to timing issues in test environment") } // Verify the state change was logged assert.True(suite.T(), suite.logContains(`"from":"open"`), "Log should mention transition from open state") assert.True(suite.T(), suite.logContains(`"to":"half-open"`), "Log should mention transition to half-open state") // 6. Execute successful requests in half-open state to transition back to closed for i := 0; i < cfg.CircuitBreaker.MaxRequestsInHalfOpen; i++ { _, err = cb.Execute(func() (any, error) { return "success", nil }) assert.NoError(suite.T(), err, "Execute should not return error") } // 7. Circuit should now be closed again assert.Equal(suite.T(), gobreaker.StateClosed.String(), cb.State().String(), "Circuit should transition to closed state after successes") // Verify the final state change was logged assert.True(suite.T(), suite.logContains(`"from":"half-open"`), "Log should mention transition from half-open state") assert.True(suite.T(), suite.logContains(`"to":"closed"`), "Log should mention transition to closed state") } // TestCircuitBreakerHalfOpenToOpen tests that the circuit transitions from half-open to open // when failures occur during half-open state func (suite *CircuitBreakerTestSuite) TestCircuitBreakerHalfOpenToOpen() { // Reset the buffer before the test suite.outputBuffer.Reset() // Initialize circuit breaker with a shorter timeout for testing cfg.CircuitBreaker.Timeout = 1 // 1 second timeout to half-open state cfg.CircuitBreaker.MaxFailures = 3 cfg.CircuitBreaker.MaxRequestsInHalfOpen = 2 initCircuitBreaker(cfg) // 1. Generate failures to trip the circuit testErr := errors.New("test error") for i := 0; i < cfg.CircuitBreaker.MaxFailures; i++ { _, err := cb.Execute(func() (any, error) { return nil, testErr }) assert.Error(suite.T(), err, "Execute should return error") } // 2. Circuit should now be open assert.Equal(suite.T(), gobreaker.StateOpen.String(), cb.State().String(), "Circuit should be open after failures") // 3. Wait for timeout to allow transition to half-open time.Sleep(time.Duration(cfg.CircuitBreaker.Timeout+1) * time.Second) // The next request should transition the circuit to half-open tmpState := cb.State() // Try a request that will fail _, _ = cb.Execute(func() (any, error) { return nil, testErr }) // 4. If we successfully reached half-open state, verify it transitions back to open after failure if tmpState.String() == gobreaker.StateHalfOpen.String() { assert.Equal(suite.T(), gobreaker.StateOpen.String(), cb.State().String(), "Circuit should transition back to open state after failure in half-open") // Verify the state changes were logged assert.True(suite.T(), suite.logContains(`"from":"open"`), "Log should mention transition from open state") assert.True(suite.T(), suite.logContains(`"to":"half-open"`), "Log should mention transition to half-open state") assert.True(suite.T(), suite.logContains(`"from":"half-open"`), "Log should mention transition from half-open state") assert.True(suite.T(), suite.logContains(`"to":"open"`), "Log should mention transition back to open state") } else { suite.T().Skip("Circuit didn't transition to half-open as expected, likely due to timing issues in test environment") } }