diff --git a/backend_health.go b/backend_health.go index d974bf4..883b68b 100644 --- a/backend_health.go +++ b/backend_health.go @@ -28,6 +28,7 @@ type BackendHealthManager struct { consecutiveFails atomic.Int32 isHealthy atomic.Bool startupProbe bool + readyOnce sync.Once } // NewBackendHealthManager creates a new backend health manager @@ -75,7 +76,7 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro "time_taken": time.Since(deadline.Add(-timeout)).String(), }, }) - close(bhm.readinessChan) + bhm.readyOnce.Do(func() { close(bhm.readinessChan) }) return nil } @@ -98,6 +99,11 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro } } + // Startup probe timed out. Main continues and starts the proxy anyway, and + // StartHealthChecking's goroutine is blocked on readinessChan — so release + // it here too, otherwise periodic health checks never begin and the + // backend can never be marked healthy again even after it recovers. + bhm.readyOnce.Do(func() { close(bhm.readinessChan) }) return fmt.Errorf("GraphQL backend did not become ready within %v", timeout) } diff --git a/connection_resilience_test.go b/connection_resilience_test.go index bb1b99c..c87b1db 100644 --- a/connection_resilience_test.go +++ b/connection_resilience_test.go @@ -265,3 +265,21 @@ func (suite *ConnectionResilienceTestSuite) TestRetryLogic() { func TestConnectionResilienceSuite(t *testing.T) { suite.Run(t, new(ConnectionResilienceTestSuite)) } + +// TestHealthChecksStartAfterStartupTimeout verifies that when the startup +// readiness probe times out, readinessChan is still released so the +// health-check goroutine (started by StartHealthChecking) proceeds and begins +// periodic checking — otherwise the backend could never be marked healthy +// again even after it recovers. +func (suite *ConnectionResilienceTestSuite) TestHealthChecksStartAfterStartupTimeout() { + bhm := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", cfg.Logger) + err := bhm.WaitForBackendReady(200 * time.Millisecond) + suite.Error(err, "expected timeout for unreachable backend") + + select { + case <-bhm.readinessChan: + // Released -> periodic health checks will begin. + case <-time.After(1 * time.Second): + suite.Fail("readinessChan not closed after startup timeout; health checks would never begin") + } +}