Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions check/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
94 changes: 94 additions & 0 deletions check/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
Expand Down Expand Up @@ -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)
}
}