From 14a0d6641060f05b4556f64acf033d95ca8e7dea Mon Sep 17 00:00:00 2001 From: Marco Jantke Date: Fri, 18 Aug 2017 15:34:04 +0200 Subject: [PATCH] Add configurable timeouts and curate default timeout settings --- cmd/traefik/traefik.go | 26 --- docs/toml.md | 77 +++++++- .../fixtures/timeout/forwarding_timeouts.toml | 38 ++++ integration/integration_test.go | 1 + integration/resources/compose/timeout.yml | 7 + integration/timeout_test.go | 44 +++++ server/configuration.go | 36 +++- server/server.go | 175 +++++++++++++----- server/server_test.go | 80 ++++++++ traefik.sample.toml | 52 ++++++ 10 files changed, 460 insertions(+), 76 deletions(-) create mode 100644 integration/fixtures/timeout/forwarding_timeouts.toml create mode 100644 integration/resources/compose/timeout.yml create mode 100644 integration/timeout_test.go diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 842629eb2..870e82ead 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -1,12 +1,9 @@ package main import ( - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" fmtlog "log" - "net/http" "os" "path/filepath" "reflect" @@ -29,7 +26,6 @@ import ( "github.com/coreos/go-systemd/daemon" "github.com/docker/libkv/store" "github.com/satori/go.uuid" - "golang.org/x/net/http2" ) func main() { @@ -178,28 +174,6 @@ func run(traefikConfiguration *server.TraefikConfiguration) { // load global configuration globalConfiguration := traefikConfiguration.GlobalConfiguration - http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost - if globalConfiguration.InsecureSkipVerify { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - - if len(globalConfiguration.RootCAs) > 0 { - roots := x509.NewCertPool() - for _, cert := range globalConfiguration.RootCAs { - certContent, err := cert.Read() - if err != nil { - log.Error("Error while read RootCAs", err) - continue - } - roots.AppendCertsFromPEM(certContent) - } - - tr := http.DefaultTransport.(*http.Transport) - tr.TLSClientConfig = &tls.Config{RootCAs: roots} - - http2.ConfigureTransport(tr) - } - if globalConfiguration.File != nil && len(globalConfiguration.File.Filename) == 0 { // no filename, setting to global config file if len(traefikConfiguration.ConfigFile) != 0 { diff --git a/docs/toml.md b/docs/toml.md index 205fdba95..5c42d67b9 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -68,7 +68,12 @@ # # ProvidersThrottleDuration = "2s" -# IdleTimeout: maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. +# IdleTimeout +# +# Deprecated - see [respondingTimeouts] section. In the case both settings are configured, the deprecated option will +# be overwritten. +# +# IdleTimeout is the maximum amount of time an idle (keep-alive) connection will remain idle before closing itself. # This is set to enforce closing of stale client connections. # Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw # values (digits). If no units are provided, the value is parsed assuming seconds. @@ -330,6 +335,76 @@ To write JSON format logs, specify `json` as the format: # interval = "30s" ``` +## Responding timeouts +``` +# respondingTimeouts are timeouts for incoming requests to the Traefik instance. +# +# Optional +# +[respondingTimeouts] + +# readTimeout is the maximum duration for reading the entire request, including the body. +# If zero, no timeout exists. +# Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw +# values (digits). If no units are provided, the value is parsed assuming seconds. +# +# Optional +# Default: "0s" +# +# readTimeout = "5s" + +# writeTimeout is the maximum duration before timing out writes of the response. It covers the time from the end of +# the request header read to the end of the response write. +# If zero, no timeout exists. +# Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw +# values (digits). If no units are provided, the value is parsed assuming seconds. +# +# Optional +# Default: "0s" +# +# writeTimeout = "5s" + +# idleTimeout is the maximum duration an idle (keep-alive) connection will remain idle before closing itself. +# If zero, no timeout exists. +# Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw +# values (digits). If no units are provided, the value is parsed assuming seconds. +# +# Optional +# Default: "180s" +# +# idleTimeout = "360s" + +``` + +## Forwarding timeouts +``` +# forwardingTimeouts are timeouts for requests forwarded to the backend servers. +# +# Optional +# +[forwardingTimeouts] + +# dialTimeout is the amount of time to wait until a connection to a backend server can be established. +# If zero, no timeout exists. +# Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw +# values (digits). If no units are provided, the value is parsed assuming seconds. +# +# Optional +# Default: "30s" +# +# dialTimeout = "30s" + +# responseHeaderTimeout is the amount of time to wait for a server's response headers after fully writing the request (including its body, if any). +# If zero, no timeout exists. +# Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw +# values (digits). If no units are provided, the value is parsed assuming seconds. +# +# Optional +# Default: "0s" +# +# responseHeaderTimeout = "0s" +``` + ## ACME (Let's Encrypt) configuration ```toml diff --git a/integration/fixtures/timeout/forwarding_timeouts.toml b/integration/fixtures/timeout/forwarding_timeouts.toml new file mode 100644 index 000000000..800e16332 --- /dev/null +++ b/integration/fixtures/timeout/forwarding_timeouts.toml @@ -0,0 +1,38 @@ +logLevel = "DEBUG" +defaultEntryPoints = ["http"] + +[entryPoints] + [entryPoints.http] + address = ":8000" + +[accessLog] +format = "json" + +[web] +address = ":8080" + +[forwardingTimeouts] +dialTimeout = "300ms" +responseHeaderTimeout = "300ms" + +[file] + +[backends] + [backends.backend1] + [backends.backend1.servers.server1] + # Non-routable IP address that should always deliver a dial timeout. + # See: https://stackoverflow.com/questions/100841/artificially-create-a-connection-timeout-error#answer-904609 + url = "http://10.255.255.1" + [backends.backend2] + [backends.backend2.servers.server2] + url = "http://{{.TimeoutEndpoint}}:9000" + +[frontends] + [frontends.frontend1] + backend = "backend1" + [frontends.frontend1.routes.test_1] + rule = "Path:/dialTimeout" + [frontends.frontend2] + backend = "backend2" + [frontends.frontend2.routes.test_2] + rule = "Path:/responseHeaderTimeout" diff --git a/integration/integration_test.go b/integration/integration_test.go index e0cbb3ff5..505463367 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -25,6 +25,7 @@ func init() { check.Suite(&SimpleSuite{}) check.Suite(&AccessLogSuite{}) check.Suite(&HTTPSSuite{}) + check.Suite(&TimeoutSuite{}) check.Suite(&FileSuite{}) check.Suite(&HealthCheckSuite{}) check.Suite(&DockerSuite{}) diff --git a/integration/resources/compose/timeout.yml b/integration/resources/compose/timeout.yml new file mode 100644 index 000000000..bf7f9c1c7 --- /dev/null +++ b/integration/resources/compose/timeout.yml @@ -0,0 +1,7 @@ +timeoutEndpoint: + image: yaman/timeout + environment: + - PROTO=http + - PORT=9000 + ports: + - "9000:9000" diff --git a/integration/timeout_test.go b/integration/timeout_test.go new file mode 100644 index 000000000..af13db1d8 --- /dev/null +++ b/integration/timeout_test.go @@ -0,0 +1,44 @@ +package integration + +import ( + "net/http" + "os" + "time" + + "github.com/containous/traefik/integration/try" + "github.com/go-check/check" + checker "github.com/vdemeester/shakers" +) + +type TimeoutSuite struct{ BaseSuite } + +func (s *TimeoutSuite) SetUpSuite(c *check.C) { + s.createComposeProject(c, "timeout") + s.composeProject.Start(c) +} + +func (s *TimeoutSuite) TestForwardingTimeouts(c *check.C) { + httpTimeoutEndpoint := s.composeProject.Container(c, "timeoutEndpoint").NetworkSettings.IPAddress + file := s.adaptFile(c, "fixtures/timeout/forwarding_timeouts.toml", struct { + TimeoutEndpoint string + }{httpTimeoutEndpoint}) + defer os.Remove(file) + + cmd, _ := s.cmdTraefik(withConfigFile(file)) + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + err = try.GetRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, try.BodyContains("Path:/dialTimeout")) + c.Assert(err, checker.IsNil) + + // This simulates a DialTimeout when connecting to the backend server. + response, err := http.Get("http://127.0.0.1:8000/dialTimeout") + c.Assert(err, checker.IsNil) + c.Assert(response.StatusCode, checker.Equals, http.StatusGatewayTimeout) + + // This simulates a ResponseHeaderTimeout. + response, err = http.Get("http://127.0.0.1:8000/responseHeaderTimeout?sleep=1000") + c.Assert(err, checker.IsNil) + c.Assert(response.StatusCode, checker.Equals, http.StatusGatewayTimeout) +} diff --git a/server/configuration.go b/server/configuration.go index 0b28b6810..a85d8ab7b 100644 --- a/server/configuration.go +++ b/server/configuration.go @@ -28,8 +28,15 @@ import ( "github.com/containous/traefik/types" ) -// DefaultHealthCheckInterval is the default health check interval. -const DefaultHealthCheckInterval = 30 * time.Second +const ( + // DefaultHealthCheckInterval is the default health check interval. + DefaultHealthCheckInterval = 30 * time.Second + + // DefaultDialTimeout when connecting to a backend server. + DefaultDialTimeout = 30 * time.Second + // DefaultIdleTimeout before closing an idle connection. + DefaultIdleTimeout = 180 * time.Second +) // TraefikConfiguration holds GlobalConfiguration and other stuff type TraefikConfiguration struct { @@ -54,11 +61,13 @@ type GlobalConfiguration struct { DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"` ProvidersThrottleDuration flaeg.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time."` MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used"` - IdleTimeout flaeg.Duration `description:"maximum amount of time an idle (keep-alive) connection will remain idle before closing itself."` + IdleTimeout flaeg.Duration `description:"(Deprecated) maximum amount of time an idle (keep-alive) connection will remain idle before closing itself."` // Deprecated InsecureSkipVerify bool `description:"Disable SSL certificate verification"` RootCAs RootCAs `description:"Add cert file for self-signed certicate"` Retry *Retry `description:"Enable retry sending request if network error"` HealthCheck *HealthCheckConfig `description:"Health check parameters"` + RespondingTimeouts *RespondingTimeouts `description:"Timeouts for incoming requests to the Traefik instance"` + ForwardingTimeouts *ForwardingTimeouts `description:"Timeouts for requests forwarded to the backend servers"` Docker *docker.Provider `description:"Enable Docker backend with default settings"` File *file.Provider `description:"Enable File backend with default settings"` Web *WebProvider `description:"Enable Web backend with default settings"` @@ -411,6 +420,19 @@ type HealthCheckConfig struct { Interval flaeg.Duration `description:"Default periodicity of enabled health checks"` } +// RespondingTimeouts contains timeout configurations for incoming requests to the Traefik instance. +type RespondingTimeouts struct { + ReadTimeout flaeg.Duration `description:"ReadTimeout is the maximum duration for reading the entire request, including the body. If zero, no timeout is set"` + WriteTimeout flaeg.Duration `description:"WriteTimeout is the maximum duration before timing out writes of the response. If zero, no timeout is set"` + IdleTimeout flaeg.Duration `description:"IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. Defaults to 180 seconds. If zero, no timeout is set"` +} + +// ForwardingTimeouts contains timeout configurations for forwarding requests to the backend servers. +type ForwardingTimeouts struct { + DialTimeout flaeg.Duration `description:"The amount of time to wait until a connection to a backend server can be established. Defaults to 30 seconds. If zero, no timeout exists"` + ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists"` +} + // NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { //default Docker @@ -573,10 +595,16 @@ func NewTraefikConfiguration() *TraefikConfiguration { DefaultEntryPoints: []string{}, ProvidersThrottleDuration: flaeg.Duration(2 * time.Second), MaxIdleConnsPerHost: 200, - IdleTimeout: flaeg.Duration(180 * time.Second), + IdleTimeout: flaeg.Duration(0), HealthCheck: &HealthCheckConfig{ Interval: flaeg.Duration(DefaultHealthCheckInterval), }, + RespondingTimeouts: &RespondingTimeouts{ + IdleTimeout: flaeg.Duration(DefaultIdleTimeout), + }, + ForwardingTimeouts: &ForwardingTimeouts{ + DialTimeout: flaeg.Duration(DefaultDialTimeout), + }, CheckNewVersion: true, }, ConfigFile: "", diff --git a/server/server.go b/server/server.go index a58f20b2e..550fa0763 100644 --- a/server/server.go +++ b/server/server.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "io/ioutil" + "net" "net/http" "net/url" "os" @@ -33,23 +34,25 @@ import ( "github.com/vulcand/oxy/forward" "github.com/vulcand/oxy/roundrobin" "github.com/vulcand/oxy/utils" + "golang.org/x/net/http2" ) var oxyLogger = &OxyLogger{} // Server is the reverse-proxy/load-balancer engine type Server struct { - serverEntryPoints serverEntryPoints - configurationChan chan types.ConfigMessage - configurationValidatedChan chan types.ConfigMessage - signals chan os.Signal - stopChan chan bool - providers []provider.Provider - currentConfigurations safe.Safe - globalConfiguration GlobalConfiguration - accessLoggerMiddleware *accesslog.LogHandler - routinesPool *safe.Pool - leadership *cluster.Leadership + serverEntryPoints serverEntryPoints + configurationChan chan types.ConfigMessage + configurationValidatedChan chan types.ConfigMessage + signals chan os.Signal + stopChan chan bool + providers []provider.Provider + currentConfigurations safe.Safe + globalConfiguration GlobalConfiguration + accessLoggerMiddleware *accesslog.LogHandler + routinesPool *safe.Pool + leadership *cluster.Leadership + defaultForwardingRoundTripper http.RoundTripper } type serverEntryPoints map[string]*serverEntryPoint @@ -82,6 +85,8 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { server.currentConfigurations.Set(currentConfigurations) server.globalConfiguration = globalConfiguration server.routinesPool = safe.NewPool(context.Background()) + server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration) + if globalConfiguration.Cluster != nil { // leadership creation if cluster mode server.leadership = cluster.NewLeadership(server.routinesPool.Ctx(), globalConfiguration.Cluster) @@ -101,6 +106,60 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { return server } +// createHTTPTransport creates an http.Transport configured with the GlobalConfiguration settings. +// For the settings that can't be configured in Traefik it uses the default http.Transport settings. +// An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHost +// in Traefik at this point in time. Setting this value to the default of 100 could lead to confusing +// behaviour and backwards compatibility issues. +func createHTTPTransport(globalConfiguration GlobalConfiguration) *http.Transport { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + } + if globalConfiguration.ForwardingTimeouts != nil { + dialer.Timeout = time.Duration(globalConfiguration.ForwardingTimeouts.DialTimeout) + } + + transport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + MaxIdleConnsPerHost: globalConfiguration.MaxIdleConnsPerHost, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + if globalConfiguration.ForwardingTimeouts != nil { + transport.ResponseHeaderTimeout = time.Duration(globalConfiguration.ForwardingTimeouts.ResponseHeaderTimeout) + } + if globalConfiguration.InsecureSkipVerify { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + if len(globalConfiguration.RootCAs) > 0 { + transport.TLSClientConfig = &tls.Config{ + RootCAs: createRootCACertPool(globalConfiguration.RootCAs), + } + http2.ConfigureTransport(transport) + } + + return transport +} + +func createRootCACertPool(rootCAs RootCAs) *x509.CertPool { + roots := x509.NewCertPool() + + for _, cert := range rootCAs { + certContent, err := cert.Read() + if err != nil { + log.Error("Error while read RootCAs", err) + continue + } + roots.AppendCertsFromPEM(certContent) + } + + return roots +} + // Start starts the server. func (server *Server) Start() { server.startHTTPServers() @@ -224,7 +283,7 @@ func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newS } serverMiddlewares = append(serverMiddlewares, ipWhitelistMiddleware) } - newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], serverMiddlewares...) + newsrv, err := server.prepareServer(newServerEntryPointName, server.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares...) if err != nil { log.Fatal("Error preparing server: ", err) } @@ -551,14 +610,17 @@ func (server *Server) startServer(srv *http.Server, globalConfiguration GlobalCo } } -func (server *Server) prepareServer(entryPointName string, router *middlewares.HandlerSwitcher, entryPoint *EntryPoint, middlewares ...negroni.Handler) (*http.Server, error) { - log.Infof("Preparing server %s %+v", entryPointName, entryPoint) +func (server *Server) prepareServer(entryPointName string, entryPoint *EntryPoint, router *middlewares.HandlerSwitcher, middlewares ...negroni.Handler) (*http.Server, error) { + readTimeout, writeTimeout, idleTimeout := buildServerTimeouts(server.globalConfiguration) + log.Infof("Preparing server %s %+v with readTimeout=%s writeTimeout=%s idleTimeout=%s", entryPointName, entryPoint, readTimeout, writeTimeout, idleTimeout) + // middlewares - var negroni = negroni.New() + n := negroni.New() for _, middleware := range middlewares { - negroni.Use(middleware) + n.Use(middleware) } - negroni.UseHandler(router) + n.UseHandler(router) + tlsConfig, err := server.createTLSConfig(entryPointName, entryPoint.TLS, router) if err != nil { log.Errorf("Error creating TLS config: %s", err) @@ -566,13 +628,37 @@ func (server *Server) prepareServer(entryPointName string, router *middlewares.H } return &http.Server{ - Addr: entryPoint.Address, - Handler: negroni, - TLSConfig: tlsConfig, - IdleTimeout: time.Duration(server.globalConfiguration.IdleTimeout), + Addr: entryPoint.Address, + Handler: n, + TLSConfig: tlsConfig, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + IdleTimeout: idleTimeout, }, nil } +func buildServerTimeouts(globalConfig GlobalConfiguration) (readTimeout, writeTimeout, idleTimeout time.Duration) { + readTimeout = time.Duration(0) + writeTimeout = time.Duration(0) + if globalConfig.RespondingTimeouts != nil { + readTimeout = time.Duration(globalConfig.RespondingTimeouts.ReadTimeout) + writeTimeout = time.Duration(globalConfig.RespondingTimeouts.WriteTimeout) + } + + // When RespondingTimeouts.IdleTimout is configured, always use that setting + if globalConfig.RespondingTimeouts != nil { + idleTimeout = time.Duration(globalConfig.RespondingTimeouts.IdleTimeout) + } else if globalConfig.IdleTimeout != 0 { + // Backwards compatibility for deprecated IdleTimeout + idleTimeout = time.Duration(globalConfig.IdleTimeout) + } else { + // Default value if neither the deprecated IdleTimeout nor the new RespondingTimeouts.IdleTimout are configured + idleTimeout = time.Duration(DefaultIdleTimeout) + } + + return readTimeout, writeTimeout, idleTimeout +} + func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]*serverEntryPoint { serverEntryPoints := make(map[string]*serverEntryPoint) for entryPointName := range globalConfiguration.EntryPoints { @@ -584,15 +670,22 @@ func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) return serverEntryPoints } -// clientTLSRoundTripper is used for forwarding client authentication to -// backend server -func clientTLSRoundTripper(config *tls.Config) http.RoundTripper { - if config == nil { - return http.DefaultTransport - } - return &http.Transport{ - TLSClientConfig: config, +// getRoundTripper will either use server.defaultForwardingRoundTripper or create a new one +// given a custom TLS configuration is passed and the passTLSCert option is set to true. +func (server *Server) getRoundTripper(globalConfiguration GlobalConfiguration, passTLSCert bool, tls *TLS) (http.RoundTripper, error) { + if passTLSCert { + tlsConfig, err := createClientTLSConfig(tls) + if err != nil { + log.Errorf("Failed to create TLSClientConfig: %s", err) + return nil, err + } + + transport := createHTTPTransport(globalConfiguration) + transport.TLSClientConfig = tlsConfig + return transport, nil } + + return server.defaultForwardingRoundTripper, nil } // LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic @@ -660,29 +753,20 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo if backends[entryPointName+frontend.Backend] == nil { log.Debugf("Creating backend %s", frontend.Backend) - var ( - tlsConfig *tls.Config - err error - lb http.Handler - ) - - if frontend.PassTLSCert { - tlsConfig, err = createClientTLSConfig(entryPoint.TLS) - if err != nil { - log.Errorf("Failed to create TLS config for frontend %s: %v", frontendName, err) - continue frontend - } + roundTripper, err := server.getRoundTripper(globalConfiguration, frontend.PassTLSCert, entryPoint.TLS) + if err != nil { + log.Errorf("Failed to create RoundTripper for frontend %s: %v", frontendName, err) + log.Errorf("Skipping frontend %s...", frontendName) + continue frontend } - // passing nil will use the roundtripper http.DefaultTransport - rt := clientTLSRoundTripper(tlsConfig) - fwd, err := forward.New( forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader), - forward.RoundTripper(rt), + forward.RoundTripper(roundTripper), forward.ErrorHandler(errorHandler), ) + if err != nil { log.Errorf("Error creating forwarder for frontend %s: %v", frontendName, err) log.Errorf("Skipping frontend %s...", frontendName) @@ -720,6 +804,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo sticky = roundrobin.NewStickySession(cookiename) } + var lb http.Handler switch lbMethod { case types.Drr: log.Debugf("Creating load-balancer drr") diff --git a/server/server_test.go b/server/server_test.go index dc3948f21..30d275256 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -36,6 +36,86 @@ func (lb *testLoadBalancer) Servers() []*url.URL { return []*url.URL{} } +func TestPrepareServerTimeouts(t *testing.T) { + tests := []struct { + desc string + globalConfig GlobalConfiguration + wantIdleTimeout time.Duration + wantReadTimeout time.Duration + wantWriteTimeout time.Duration + }{ + { + desc: "full configuration", + globalConfig: GlobalConfiguration{ + RespondingTimeouts: &RespondingTimeouts{ + IdleTimeout: flaeg.Duration(10 * time.Second), + ReadTimeout: flaeg.Duration(12 * time.Second), + WriteTimeout: flaeg.Duration(14 * time.Second), + }, + }, + wantIdleTimeout: time.Duration(10 * time.Second), + wantReadTimeout: time.Duration(12 * time.Second), + wantWriteTimeout: time.Duration(14 * time.Second), + }, + { + desc: "using defaults", + globalConfig: GlobalConfiguration{}, + wantIdleTimeout: time.Duration(180 * time.Second), + wantReadTimeout: time.Duration(0 * time.Second), + wantWriteTimeout: time.Duration(0 * time.Second), + }, + { + desc: "deprecated IdleTimeout configured", + globalConfig: GlobalConfiguration{ + IdleTimeout: flaeg.Duration(45 * time.Second), + }, + wantIdleTimeout: time.Duration(45 * time.Second), + wantReadTimeout: time.Duration(0 * time.Second), + wantWriteTimeout: time.Duration(0 * time.Second), + }, + { + desc: "deprecated and new IdleTimeout configured", + globalConfig: GlobalConfiguration{ + IdleTimeout: flaeg.Duration(45 * time.Second), + RespondingTimeouts: &RespondingTimeouts{ + IdleTimeout: flaeg.Duration(80 * time.Second), + }, + }, + wantIdleTimeout: time.Duration(80 * time.Second), + wantReadTimeout: time.Duration(0 * time.Second), + wantWriteTimeout: time.Duration(0 * time.Second), + }, + } + + for _, test := range tests { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + entryPointName := "http" + entryPoint := &EntryPoint{Address: "localhost:8080"} + router := middlewares.NewHandlerSwitcher(mux.NewRouter()) + + srv := NewServer(test.globalConfig) + httpServer, err := srv.prepareServer(entryPointName, entryPoint, router) + if err != nil { + t.Fatalf("Unexpected error when preparing srv: %s", err) + } + + if httpServer.IdleTimeout != test.wantIdleTimeout { + t.Errorf("Got %s as IdleTimeout, want %s", httpServer.IdleTimeout, test.wantIdleTimeout) + } + if httpServer.ReadTimeout != test.wantReadTimeout { + t.Errorf("Got %s as ReadTimeout, want %s", httpServer.ReadTimeout, test.wantReadTimeout) + } + if httpServer.WriteTimeout != test.wantWriteTimeout { + t.Errorf("Got %s as WriteTimeout, want %s", httpServer.WriteTimeout, test.wantWriteTimeout) + } + }) + } +} + func TestServerMultipleFrontendRules(t *testing.T) { cases := []struct { expression string diff --git a/traefik.sample.toml b/traefik.sample.toml index 072d63327..fa58eaeea 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -366,6 +366,58 @@ # # interval = "30s" +# Timeout settings for the http servers Traefik starts +# +# Optional +# +# [respondingTimeouts] + +# ReadTimeout is the maximum duration for reading the entire request, including the body. +# If zero, no timeout exists. +# +# Optional +# Default: "0s" +# +# readTimeout = "5s" + +# WriteTimeout is the maximum duration before timing out writes of the response. +# If zero, no timeout exists. +# +# Optional +# Default: "0s" +# +# writeTimeout = "5s" + +# IdleTimeout is the maximum amount duration an idle (keep-alive) connection will remain idle before closing itself. +# Defaults to 180 seconds. +# If zero, no timeout exists. +# +# Optional +# Default: "180s" +# +# idleTimeout = "360s" + +# Timeout settings for requests forwarded to the Backend Servers +# +# Optional +# +# [forwardingTimeouts] + +# The amount of time to wait until a connection to a Backend Server can be established. +# If zero, no timeout exists. +# +# Optional +# Default: "30s" +# +# dialTimeout = "30s" + +# The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists +# +# Optional +# Default: "0s" +# +# responseHeaderTimeout = "0s" + ################################################################ # Web configuration backend ################################################################