diff --git a/check/http/http.go b/check/http/http.go index f9b6b5c..ea23e14 100644 --- a/check/http/http.go +++ b/check/http/http.go @@ -62,6 +62,11 @@ type Checker struct { // occurs between attempts. AttemptSpacing time.Duration `json:"attempt_spacing,omitempty"` + // AllowedFailures is the number of failed attempts + // allowed before the endpoint is considered down. + // Default is 0 (any failure marks down). + AllowedFailures int `json:"allowed_failures,omitempty" yaml:"allowed_failures,omitempty"` + // Client is the http.Client with which to make // requests. If not set, DefaultHTTPClient is // used. @@ -149,12 +154,20 @@ func (c Checker) conclude(result types.Result) types.Result { result.ThresholdRTT = c.ThresholdRTT // Check errors (down) + var errorCount int for i := range result.Times { if result.Times[i].Error != "" { - result.Down = true - return result + errorCount++ } } + allowedFailures := c.AllowedFailures + if allowedFailures < 0 { + allowedFailures = 0 + } + if errorCount > allowedFailures { + result.Down = true + return result + } // Check round trip time (degraded) if c.ThresholdRTT > 0 { diff --git a/check/http/http_test.go b/check/http/http_test.go index 5ad4348..c3637f1 100644 --- a/check/http/http_test.go +++ b/check/http/http_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "sync/atomic" "testing" "time" ) @@ -147,3 +148,96 @@ func TestChecker(t *testing.T) { t.Errorf("Expected result.Down=%v, got %v", want, got) } } + +func TestCheckerAllowedFailures(t *testing.T) { + var requestCount int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count := atomic.AddInt32(&requestCount, 1) + if count == 1 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") + })) + defer srv.Close() + + // 1. Default behavior (AllowedFailures: 0) marks down on single failure. + hcDefault := Checker{ + Name: "Default Failures", + URL: srv.URL, + Attempts: 3, + AllowedFailures: 0, + } + atomic.StoreInt32(&requestCount, 0) + result, err := hcDefault.Check() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Down { + t.Errorf("expected result.Down=true with default AllowedFailures=0 and 1 failure, got %v", result.Down) + } + if result.Healthy { + t.Errorf("expected result.Healthy=false, got %v", result.Healthy) + } + + // 2. AllowedFailures: 1 permits 1 failed attempt out of N attempts without marking down. + hcAllowed := Checker{ + Name: "Allowed 1 Failure", + URL: srv.URL, + Attempts: 3, + AllowedFailures: 1, + } + atomic.StoreInt32(&requestCount, 0) + result, err = hcAllowed.Check() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.Down { + t.Errorf("expected result.Down=false with AllowedFailures=1 and 1 failure, got %v", result.Down) + } + if !result.Healthy { + t.Errorf("expected result.Healthy=true with AllowedFailures=1 and 1 failure, got %v", result.Healthy) + } + + // 3. Errors exceeding AllowedFailures properly mark down. + var multiFailCount int32 + srvMultiFail := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count := atomic.AddInt32(&multiFailCount, 1) + if count <= 2 { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, "OK") + })) + defer srvMultiFail.Close() + + hcExceeded := Checker{ + Name: "Exceeded Failures", + URL: srvMultiFail.URL, + Attempts: 3, + AllowedFailures: 1, + } + result, err = hcExceeded.Check() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !result.Down { + t.Errorf("expected result.Down=true when errors exceed AllowedFailures, got %v", result.Down) + } + if result.Healthy { + t.Errorf("expected result.Healthy=false when errors exceed AllowedFailures, got %v", result.Healthy) + } +} + +func TestCheckerUnmarshalAllowedFailures(t *testing.T) { + raw := []byte(`{"endpoint_name":"test","endpoint_url":"http://localhost","allowed_failures":2,"attempts":5}`) + c, err := New(raw) + if err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } + if c.AllowedFailures != 2 { + t.Errorf("expected AllowedFailures=2, got %d", c.AllowedFailures) + } +}