package health import ( "context" "fmt" "net/http" "time" ) // DefaultRetries is the number of health check attempts before declaring failure. const DefaultRetries = 3 // DefaultRetryInterval is the pause between health check retries. const DefaultRetryInterval = 5 * time.Second // DefaultTimeout is the HTTP timeout for a single health check attempt. const DefaultTimeout = 10 * time.Second // Checker performs HTTP health checks against a container endpoint. type Checker struct { httpClient *http.Client retries int retryInterval time.Duration } // New creates a Checker with default settings. func New() *Checker { return &Checker{ httpClient: &http.Client{ Timeout: DefaultTimeout, }, retries: DefaultRetries, retryInterval: DefaultRetryInterval, } } // Check performs an HTTP GET health check against the given URL. // It retries up to the configured number of times, waiting retryInterval between attempts. // Returns nil on the first successful (2xx) response, or the last error encountered. func (c *Checker) Check(ctx context.Context, url string) error { var lastErr error for attempt := 0; attempt < c.retries; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return fmt.Errorf("health check cancelled: %w", ctx.Err()) case <-time.After(c.retryInterval): } } lastErr = c.doCheck(ctx, url) if lastErr == nil { return nil } } return fmt.Errorf("health check failed after %d attempts: %w", c.retries, lastErr) } // doCheck performs a single HTTP GET health check. func (c *Checker) doCheck(ctx context.Context, url string) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("create health check request: %w", err) } resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("health check request to %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("health check %s returned status %d", url, resp.StatusCode) } return nil }