Repository: traefik/traefik (language: go)

Issue:
Feature: allow healthcheck interval shorter than timeout
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you expect to see?

According to the documentation, a service healthcheck timeout must be less than the healthcheck interval. In Traefik 2.x, a shorter interval [generates a warning](https://github.com/traefik/traefik/blob/v2.9.9/pkg/server/service/service.go#L354-L356), but is permitted. However in 3.x, the restriction [is enforced](https://github.com/traefik/traefik/blob/master/pkg/healthcheck/healthcheck.go#L74).

I have an application where I'd like to use a frequent healthcheck (1s), but also allow for the case where that healthcheck may _occasionally_ take longer than 1s to respond. I'd rather not decrease the check frequency, but I also don't want a slow healthcheck to be treated as failing. I think this is a valid approach.

The behaviour is working well on 2.x, but it goes against what the documentation (and warning) states. And as mentioned, it looks like this will stop working in 3.x.

Could we remove this restriction?


Discussion / hints:
Hello @kevinmcconnell,

Thanks for reporting this!

We agree. The timeout and the interval are not related. We think that the v2 behavior should be brought back on v3 and that on both versions the warning should be removed from the documentation. And we should remove as well the warning from the logs. This would make the documentation and the behavior consistent.
@rtribotte That sounds perfect. Thanks for the quick reply!

Fix patch:
diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md
index c809392f46..fb6af75639 100644
--- a/docs/content/routing/services/index.md
+++ b/docs/content/routing/services/index.md
@@ -338,7 +338,6 @@ Below are the available options for the health check mechanism:
 !!! info "Interval & Timeout Format"
 
     Interval and timeout are to be given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration).
-    The interval must be greater than the timeout. If configuration doesn't reflect this, the interval will be set to timeout + 1 second.
 
 !!! info "Recovering Servers"
 
diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go
index 1452bfa665..747999889a 100644
--- a/pkg/healthcheck/healthcheck.go
+++ b/pkg/healthcheck/healthcheck.go
@@ -69,11 +69,6 @@ func NewServiceHealthChecker(ctx context.Context, metrics metricsHealthCheck, co
 		timeout = time.Duration(dynamic.DefaultHealthCheckTimeout)
 	}
 
-	if timeout >= interval {
-		logger.Warn().Msgf("Health check timeout should be lower than the health check interval. Interval set to timeout + 1 second (%s).", interval)
-		interval = timeout + time.Second
-	}
-
 	client := &http.Client{
 		Transport: transport,
 	}


Test patch:
diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go
index c7813fae09..52b70448c7 100644
--- a/pkg/healthcheck/healthcheck_test.go
+++ b/pkg/healthcheck/healthcheck_test.go
@@ -18,6 +18,57 @@ import (
 	healthpb "google.golang.org/grpc/health/grpc_health_v1"
 )
 
+func TestNewServiceHealthChecker_durations(t *testing.T) {
+	testCases := []struct {
+		desc        string
+		config      *dynamic.ServerHealthCheck
+		expInterval time.Duration
+		expTimeout  time.Duration
+	}{
+		{
+			desc:        "default values",
+			config:      &dynamic.ServerHealthCheck{},
+			expInterval: time.Duration(dynamic.DefaultHealthCheckInterval),
+			expTimeout:  time.Duration(dynamic.DefaultHealthCheckTimeout),
+		},
+		{
+			desc: "out of range values",
+			config: &dynamic.ServerHealthCheck{
+				Interval: ptypes.Duration(-time.Second),
+				Timeout:  ptypes.Duration(-time.Second),
+			},
+			expInterval: time.Duration(dynamic.DefaultHealthCheckInterval),
+			expTimeout:  time.Duration(dynamic.DefaultHealthCheckTimeout),
+		},
+		{
+			desc: "custom durations",
+			config: &dynamic.ServerHealthCheck{
+				Interval: ptypes.Duration(time.Second * 10),
+				Timeout:  ptypes.Duration(time.Second * 5),
+			},
+			expInterval: time.Second * 10,
+			expTimeout:  time.Second * 5,
+		},
+		{
+			desc: "interval shorter than timeout",
+			config: &dynamic.ServerHealthCheck{
+				Interval: ptypes.Duration(time.Second),
+				Timeout:  ptypes.Duration(time.Second * 5),
+			},
+			expInterval: time.Second,
+			expTimeout:  time.Second * 5,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.desc, func(t *testing.T) {
+			healthChecker := NewServiceHealthChecker(context.Background(), nil, test.config, nil, nil, http.DefaultTransport, nil)
+			assert.Equal(t, test.expInterval, healthChecker.interval)
+			assert.Equal(t, test.expTimeout, healthChecker.timeout)
+		})
+	}
+}
+
 func TestServiceHealthChecker_newRequest(t *testing.T) {
 	testCases := []struct {
 		desc        string