Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support all 2xx HTTP status code for health check. #3362

Merged
merged 1 commit into from
May 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/basics.md
Expand Up @@ -454,13 +454,12 @@ The deprecated way:

#### Health Check

A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `200 OK` to HTTP GET requests periodically carried out by Traefik.
A health check can be configured in order to remove a backend from LB rotation as long as it keeps returning HTTP status codes other than `2xx` to HTTP GET requests periodically carried out by Traefik.
The check is defined by a path appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how often the health check should be executed (the default being 30 seconds).
Each backend must respond to the health check within 5 seconds.
By default, the port of the backend server is used, however, this may be overridden.

A recovering backend returning 200 OK responses again is being returned to the
LB rotation pool.
A recovering backend returning `2xx` responses again is being returned to the LB rotation pool.

For example:
```toml
Expand Down
25 changes: 14 additions & 11 deletions healthcheck/healthcheck.go
Expand Up @@ -174,6 +174,7 @@ func (b *BackendHealthCheck) addHeadersAndHost(req *http.Request) *http.Request
if b.Options.Hostname != "" {
req.Host = b.Options.Hostname
}

for k, v := range b.Options.Headers {
req.Header.Set(k, v)
}
Expand All @@ -183,26 +184,28 @@ func (b *BackendHealthCheck) addHeadersAndHost(req *http.Request) *http.Request
// checkHealth returns a nil error in case it was successful and otherwise
// a non-nil error with a meaningful description why the health check failed.
func checkHealth(serverURL *url.URL, backend *BackendHealthCheck) error {
client := http.Client{
Timeout: backend.requestTimeout,
Transport: backend.Options.Transport,
}
req, err := backend.newRequest(serverURL)
if err != nil {
return fmt.Errorf("failed to create HTTP request: %s", err)
}

req = backend.addHeadersAndHost(req)

resp, err := client.Do(req)
if err == nil {
defer resp.Body.Close()
client := http.Client{
Timeout: backend.requestTimeout,
Transport: backend.Options.Transport,
}

switch {
case err != nil:
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("HTTP request failed: %s", err)
case resp.StatusCode != http.StatusOK:
return fmt.Errorf("received non-200 status code: %v", resp.StatusCode)
}

defer resp.Body.Close()

if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("received non-2xx status code: %v", resp.StatusCode)
}

return nil
}
38 changes: 20 additions & 18 deletions healthcheck/healthcheck_test.go
Expand Up @@ -19,54 +19,62 @@ const healthCheckInterval = 100 * time.Millisecond

type testHandler struct {
done func()
healthSequence []bool
healthSequence []int
}

func TestSetBackendsConfiguration(t *testing.T) {
testCases := []struct {
desc string
startHealthy bool
healthSequence []bool
healthSequence []int
expectedNumRemovedServers int
expectedNumUpsertedServers int
expectedGaugeValue float64
}{
{
desc: "healthy server staying healthy",
startHealthy: true,
healthSequence: []bool{true},
healthSequence: []int{http.StatusOK},
expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0,
expectedGaugeValue: 1,
},
{
desc: "healthy server staying healthy (StatusNoContent)",
startHealthy: true,
healthSequence: []int{http.StatusNoContent},
expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0,
expectedGaugeValue: 1,
},
{
desc: "healthy server becoming sick",
startHealthy: true,
healthSequence: []bool{false},
healthSequence: []int{http.StatusServiceUnavailable},
expectedNumRemovedServers: 1,
expectedNumUpsertedServers: 0,
expectedGaugeValue: 0,
},
{
desc: "sick server becoming healthy",
startHealthy: false,
healthSequence: []bool{true},
healthSequence: []int{http.StatusOK},
expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 1,
expectedGaugeValue: 1,
},
{
desc: "sick server staying sick",
startHealthy: false,
healthSequence: []bool{false},
healthSequence: []int{http.StatusServiceUnavailable},
expectedNumRemovedServers: 0,
expectedNumUpsertedServers: 0,
expectedGaugeValue: 0,
},
{
desc: "healthy server toggling to sick and back to healthy",
startHealthy: true,
healthSequence: []bool{false, true},
healthSequence: []int{http.StatusServiceUnavailable, http.StatusOK},
expectedNumRemovedServers: 1,
expectedNumUpsertedServers: 1,
expectedGaugeValue: 1,
Expand All @@ -77,6 +85,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()

// The context is passed to the health check and canonically cancelled by
// the test server once all expected requests have been received.
ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -320,29 +329,22 @@ func (lb *testLoadBalancer) removeServer(u *url.URL) {
lb.servers = append(lb.servers[:i], lb.servers[i+1:]...)
}

func newTestServer(done func(), healthSequence []bool) *httptest.Server {
func newTestServer(done func(), healthSequence []int) *httptest.Server {
handler := &testHandler{
done: done,
healthSequence: healthSequence,
}
return httptest.NewServer(handler)
}

// ServeHTTP returns 200 or 503 HTTP response codes depending on whether the
// current request is marked as healthy or not.
// It calls the given 'done' function once all request health indicators have
// been depleted.
// ServeHTTP returns HTTP response codes following a status sequences.
// It calls the given 'done' function once all request health indicators have been depleted.
func (th *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if len(th.healthSequence) == 0 {
panic("received unexpected request")
}

healthy := th.healthSequence[0]
if healthy {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
w.WriteHeader(th.healthSequence[0])

th.healthSequence = th.healthSequence[1:]
if len(th.healthSequence) == 0 {
Expand Down