Add support for configuring Envoys route idle_timeout (#14340)

* Add idleTimeout

Co-authored-by: Jeff Boruszak <104028618+boruszak@users.noreply.github.com>
Co-authored-by: Dhia Ayachi <dhia@hashicorp.com>
This commit is contained in:
James Oulman 2022-11-29 17:43:15 -05:00 committed by GitHub
parent ecd4307b7c
commit 71f7f2e3dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 144 additions and 1 deletions

4
.changelog/14340.txt Normal file
View File

@ -0,0 +1,4 @@
```release-note:feature
connect: Add local_idle_timeout_ms to allow configuring the Envoy route idle timeout on local_app
connect: Add IdleTimeout to service-router to allow configuring the Envoy route idle timeout
```

View File

@ -4107,6 +4107,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
"namespace" : "leek", "namespace" : "leek",
"prefix_rewrite" : "/alternate", "prefix_rewrite" : "/alternate",
"request_timeout" : "99s", "request_timeout" : "99s",
"idle_timeout" : "99s",
"num_retries" : 12345, "num_retries" : 12345,
"retry_on_connect_failure": true, "retry_on_connect_failure": true,
"retry_on_status_codes" : [401, 209] "retry_on_status_codes" : [401, 209]
@ -4195,6 +4196,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
namespace = "leek" namespace = "leek"
prefix_rewrite = "/alternate" prefix_rewrite = "/alternate"
request_timeout = "99s" request_timeout = "99s"
idle_timeout = "99s"
num_retries = 12345 num_retries = 12345
retry_on_connect_failure = true retry_on_connect_failure = true
retry_on_status_codes = [401, 209] retry_on_status_codes = [401, 209]
@ -4284,6 +4286,7 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
Partition: acl.DefaultPartitionName, Partition: acl.DefaultPartitionName,
PrefixRewrite: "/alternate", PrefixRewrite: "/alternate",
RequestTimeout: 99 * time.Second, RequestTimeout: 99 * time.Second,
IdleTimeout: 99 * time.Second,
NumRetries: 12345, NumRetries: 12345,
RetryOnConnectFailure: true, RetryOnConnectFailure: true,
RetryOnStatusCodes: []uint32{401, 209}, RetryOnStatusCodes: []uint32{401, 209},

View File

@ -682,6 +682,15 @@ func setupTestVariationDiscoveryChain(
RequestTimeout: 33 * time.Second, RequestTimeout: 33 * time.Second,
}, },
}, },
{
Match: httpMatch(&structs.ServiceRouteHTTPMatch{
PathPrefix: "/idle-timeout",
}),
Destination: &structs.ServiceRouteDestination{
Service: "idle-timeout",
IdleTimeout: 33 * time.Second,
},
},
{ {
Match: httpMatch(&structs.ServiceRouteHTTPMatch{ Match: httpMatch(&structs.ServiceRouteHTTPMatch{
PathPrefix: "/retry-connect", PathPrefix: "/retry-connect",

View File

@ -426,6 +426,10 @@ type ServiceRouteDestination struct {
// downstream request (and retries) to be processed. // downstream request (and retries) to be processed.
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"` RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
// IdleTimeout is The total amount of time permitted for the request stream
// to be idle
IdleTimeout time.Duration `json:",omitempty" alias:"idle_timeout"`
// NumRetries is the number of times to retry the request when a retryable // NumRetries is the number of times to retry the request when a retryable
// result occurs. This seems fairly proxy agnostic. // result occurs. This seems fairly proxy agnostic.
NumRetries uint32 `json:",omitempty" alias:"num_retries"` NumRetries uint32 `json:",omitempty" alias:"num_retries"`
@ -452,15 +456,21 @@ func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
type Alias ServiceRouteDestination type Alias ServiceRouteDestination
exported := &struct { exported := &struct {
RequestTimeout string `json:",omitempty"` RequestTimeout string `json:",omitempty"`
IdleTimeout string `json:",omitempty"`
*Alias *Alias
}{ }{
RequestTimeout: e.RequestTimeout.String(), RequestTimeout: e.RequestTimeout.String(),
IdleTimeout: e.IdleTimeout.String(),
Alias: (*Alias)(e), Alias: (*Alias)(e),
} }
if e.RequestTimeout == 0 { if e.RequestTimeout == 0 {
exported.RequestTimeout = "" exported.RequestTimeout = ""
} }
if e.IdleTimeout == 0 {
exported.IdleTimeout = ""
}
return json.Marshal(exported) return json.Marshal(exported)
} }
@ -468,6 +478,7 @@ func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
type Alias ServiceRouteDestination type Alias ServiceRouteDestination
aux := &struct { aux := &struct {
RequestTimeout string RequestTimeout string
IdleTimeout string
*Alias *Alias
}{ }{
Alias: (*Alias)(e), Alias: (*Alias)(e),
@ -481,6 +492,11 @@ func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
return err return err
} }
} }
if aux.IdleTimeout != "" {
if e.IdleTimeout, err = time.ParseDuration(aux.IdleTimeout); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -603,6 +603,7 @@ func TestDecodeConfigEntry(t *testing.T) {
namespace = "leek" namespace = "leek"
prefix_rewrite = "/alternate" prefix_rewrite = "/alternate"
request_timeout = "99s" request_timeout = "99s"
idle_timeout = "99s"
num_retries = 12345 num_retries = 12345
retry_on_connect_failure = true retry_on_connect_failure = true
retry_on_status_codes = [401, 209] retry_on_status_codes = [401, 209]
@ -704,6 +705,7 @@ func TestDecodeConfigEntry(t *testing.T) {
Namespace = "leek" Namespace = "leek"
PrefixRewrite = "/alternate" PrefixRewrite = "/alternate"
RequestTimeout = "99s" RequestTimeout = "99s"
IdleTimeout = "99s"
NumRetries = 12345 NumRetries = 12345
RetryOnConnectFailure = true RetryOnConnectFailure = true
RetryOnStatusCodes = [401, 209] RetryOnStatusCodes = [401, 209]
@ -805,6 +807,7 @@ func TestDecodeConfigEntry(t *testing.T) {
Namespace: "leek", Namespace: "leek",
PrefixRewrite: "/alternate", PrefixRewrite: "/alternate",
RequestTimeout: 99 * time.Second, RequestTimeout: 99 * time.Second,
IdleTimeout: 99 * time.Second,
NumRetries: 12345, NumRetries: 12345,
RetryOnConnectFailure: true, RetryOnConnectFailure: true,
RetryOnStatusCodes: []uint32{401, 209}, RetryOnStatusCodes: []uint32{401, 209},

View File

@ -49,6 +49,11 @@ type ProxyConfig struct {
// respected (15s) // respected (15s)
LocalRequestTimeoutMs *int `mapstructure:"local_request_timeout_ms"` LocalRequestTimeoutMs *int `mapstructure:"local_request_timeout_ms"`
// LocalIdleTimeoutMs is the number of milliseconds to timeout HTTP streams
// to the local app instance. If not set, no value is set, Envoy defaults are
// respected (300s)
LocalIdleTimeoutMs *int `mapstructure:"local_idle_timeout_ms"`
// Protocol describes the service's protocol. Valid values are "tcp", // Protocol describes the service's protocol. Valid values are "tcp",
// "http" and "grpc". Anything else is treated as tcp. This enables // "http" and "grpc". Anything else is treated as tcp. This enables
// protocol aware features like per-request metrics and connection // protocol aware features like per-request metrics and connection

View File

@ -157,6 +157,39 @@ func TestParseProxyConfig(t *testing.T) {
Protocol: "tcp", Protocol: "tcp",
}, },
}, },
{
name: "local idle timeout override, float ",
input: map[string]interface{}{
"local_idle_timeout_ms": float64(1000.0),
},
want: ProxyConfig{
LocalConnectTimeoutMs: 5000,
LocalIdleTimeoutMs: intPointer(1000),
Protocol: "tcp",
},
},
{
name: "local idle timeout override, int ",
input: map[string]interface{}{
"local_idle_timeout_ms": 1000,
},
want: ProxyConfig{
LocalConnectTimeoutMs: 5000,
LocalIdleTimeoutMs: intPointer(1000),
Protocol: "tcp",
},
},
{
name: "local idle timeout override, string",
input: map[string]interface{}{
"local_idle_timeout_ms": "1000",
},
want: ProxyConfig{
LocalConnectTimeoutMs: 5000,
LocalIdleTimeoutMs: intPointer(1000),
Protocol: "tcp",
},
},
{ {
name: "balance inbound connections override, string", name: "balance inbound connections override, string",
input: map[string]interface{}{ input: map[string]interface{}{

View File

@ -1275,6 +1275,7 @@ func (s *ResourceGenerator) makeInboundListener(cfgSnap *proxycfg.ConfigSnapshot
routeName: name, routeName: name,
cluster: LocalAppClusterName, cluster: LocalAppClusterName,
requestTimeoutMs: cfg.LocalRequestTimeoutMs, requestTimeoutMs: cfg.LocalRequestTimeoutMs,
idleTimeoutMs: cfg.LocalIdleTimeoutMs,
tracing: tracing, tracing: tracing,
} }
if useHTTPFilter { if useHTTPFilter {
@ -2114,6 +2115,7 @@ type listenerFilterOpts struct {
statPrefix string statPrefix string
routePath string routePath string
requestTimeoutMs *int requestTimeoutMs *int
idleTimeoutMs *int
ingressGateway bool ingressGateway bool
httpAuthzFilter *envoy_http_v3.HttpFilter httpAuthzFilter *envoy_http_v3.HttpFilter
forwardClientDetails bool forwardClientDetails bool
@ -2260,6 +2262,11 @@ func makeHTTPFilter(opts listenerFilterOpts) (*envoy_listener_v3.Filter, error)
r.Timeout = durationpb.New(time.Duration(*opts.requestTimeoutMs) * time.Millisecond) r.Timeout = durationpb.New(time.Duration(*opts.requestTimeoutMs) * time.Millisecond)
} }
if opts.idleTimeoutMs != nil {
r := route.GetRoute()
r.IdleTimeout = durationpb.New(time.Duration(*opts.idleTimeoutMs) * time.Millisecond)
}
// If a path is provided, do not match on a catch-all prefix // If a path is provided, do not match on a catch-all prefix
if opts.routePath != "" { if opts.routePath != "" {
route.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Path{Path: opts.routePath} route.Match.PathSpecifier = &envoy_route_v3.RouteMatch_Path{Path: opts.routePath}

View File

@ -230,6 +230,7 @@ func TestListenersFromSnapshot(t *testing.T) {
ns.Proxy.Config["protocol"] = "http" ns.Proxy.Config["protocol"] = "http"
ns.Proxy.Config["local_connect_timeout_ms"] = 1234 ns.Proxy.Config["local_connect_timeout_ms"] = 1234
ns.Proxy.Config["local_request_timeout_ms"] = 2345 ns.Proxy.Config["local_request_timeout_ms"] = 2345
ns.Proxy.Config["local_idle_timeout_ms"] = 3456
}, nil) }, nil)
}, },
}, },

View File

@ -577,6 +577,10 @@ func (s *ResourceGenerator) makeUpstreamRouteForDiscoveryChain(
routeAction.Route.Timeout = durationpb.New(destination.RequestTimeout) routeAction.Route.Timeout = durationpb.New(destination.RequestTimeout)
} }
if destination.IdleTimeout > 0 {
routeAction.Route.IdleTimeout = durationpb.New(destination.IdleTimeout)
}
if destination.HasRetryFeatures() { if destination.HasRetryFeatures() {
routeAction.Route.RetryPolicy = getRetryPolicyForDestination(destination) routeAction.Route.RetryPolicy = getRetryPolicyForDestination(destination)
} }

View File

@ -83,7 +83,8 @@
}, },
"route": { "route": {
"cluster": "local_app", "cluster": "local_app",
"timeout": "2.345s" "timeout": "2.345s",
"idleTimeout": "3.456s"
} }
} }
] ]

View File

@ -274,6 +274,15 @@
"timeout": "33s" "timeout": "33s"
} }
}, },
{
"match": {
"prefix": "/idle-timeout"
},
"route": {
"cluster": "idle-timeout.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"idleTimeout": "33s"
}
},
{ {
"match": { "match": {
"prefix": "/retry-connect" "prefix": "/retry-connect"

View File

@ -275,6 +275,15 @@
"timeout": "33s" "timeout": "33s"
} }
}, },
{
"match": {
"prefix": "/idle-timeout"
},
"route": {
"cluster": "idle-timeout.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"idleTimeout": "33s"
}
},
{ {
"match": { "match": {
"prefix": "/retry-connect" "prefix": "/retry-connect"

View File

@ -275,6 +275,15 @@
"timeout": "33s" "timeout": "33s"
} }
}, },
{
"match": {
"prefix": "/idle-timeout"
},
"route": {
"cluster": "idle-timeout.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"idleTimeout": "33s"
}
},
{ {
"match": { "match": {
"prefix": "/retry-connect" "prefix": "/retry-connect"

View File

@ -69,6 +69,7 @@ type ServiceRouteDestination struct {
Partition string `json:",omitempty"` Partition string `json:",omitempty"`
PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"` PrefixRewrite string `json:",omitempty" alias:"prefix_rewrite"`
RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"` RequestTimeout time.Duration `json:",omitempty" alias:"request_timeout"`
IdleTimeout time.Duration `json:",omitempty" alias:"idle_timeout"`
NumRetries uint32 `json:",omitempty" alias:"num_retries"` NumRetries uint32 `json:",omitempty" alias:"num_retries"`
RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"` RetryOnConnectFailure bool `json:",omitempty" alias:"retry_on_connect_failure"`
RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"` RetryOnStatusCodes []uint32 `json:",omitempty" alias:"retry_on_status_codes"`
@ -81,14 +82,19 @@ func (e *ServiceRouteDestination) MarshalJSON() ([]byte, error) {
type Alias ServiceRouteDestination type Alias ServiceRouteDestination
exported := &struct { exported := &struct {
RequestTimeout string `json:",omitempty"` RequestTimeout string `json:",omitempty"`
IdleTimeout string `json:",omitempty"`
*Alias *Alias
}{ }{
RequestTimeout: e.RequestTimeout.String(), RequestTimeout: e.RequestTimeout.String(),
IdleTimeout: e.IdleTimeout.String(),
Alias: (*Alias)(e), Alias: (*Alias)(e),
} }
if e.RequestTimeout == 0 { if e.RequestTimeout == 0 {
exported.RequestTimeout = "" exported.RequestTimeout = ""
} }
if e.IdleTimeout == 0 {
exported.IdleTimeout = ""
}
return json.Marshal(exported) return json.Marshal(exported)
} }
@ -97,6 +103,7 @@ func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
type Alias ServiceRouteDestination type Alias ServiceRouteDestination
aux := &struct { aux := &struct {
RequestTimeout string RequestTimeout string
IdleTimeout string
*Alias *Alias
}{ }{
Alias: (*Alias)(e), Alias: (*Alias)(e),
@ -110,6 +117,11 @@ func (e *ServiceRouteDestination) UnmarshalJSON(data []byte) error {
return err return err
} }
} }
if aux.IdleTimeout != "" {
if e.IdleTimeout, err = time.ParseDuration(aux.IdleTimeout); err != nil {
return err
}
}
return nil return nil
} }

View File

@ -612,6 +612,7 @@ func TestDecodeConfigEntry(t *testing.T) {
"Namespace": "leek", "Namespace": "leek",
"PrefixRewrite": "/alternate", "PrefixRewrite": "/alternate",
"RequestTimeout": "99s", "RequestTimeout": "99s",
"IdleTimeout": "99s",
"NumRetries": 12345, "NumRetries": 12345,
"RetryOnConnectFailure": true, "RetryOnConnectFailure": true,
"RetryOnStatusCodes": [401, 209] "RetryOnStatusCodes": [401, 209]
@ -696,6 +697,7 @@ func TestDecodeConfigEntry(t *testing.T) {
Namespace: "leek", Namespace: "leek",
PrefixRewrite: "/alternate", PrefixRewrite: "/alternate",
RequestTimeout: 99 * time.Second, RequestTimeout: 99 * time.Second,
IdleTimeout: 99 * time.Second,
NumRetries: 12345, NumRetries: 12345,
RetryOnConnectFailure: true, RetryOnConnectFailure: true,
RetryOnStatusCodes: []uint32{401, 209}, RetryOnStatusCodes: []uint32{401, 209},

View File

@ -821,6 +821,7 @@ func TestParseConfigEntry(t *testing.T) {
partition = "chard" partition = "chard"
prefix_rewrite = "/alternate" prefix_rewrite = "/alternate"
request_timeout = "99s" request_timeout = "99s"
idle_timeout = "99s"
num_retries = 12345 num_retries = 12345
retry_on_connect_failure = true retry_on_connect_failure = true
retry_on_status_codes = [401, 209] retry_on_status_codes = [401, 209]
@ -906,6 +907,7 @@ func TestParseConfigEntry(t *testing.T) {
Partition = "chard" Partition = "chard"
PrefixRewrite = "/alternate" PrefixRewrite = "/alternate"
RequestTimeout = "99s" RequestTimeout = "99s"
IdleTimeout = "99s"
NumRetries = 12345 NumRetries = 12345
RetryOnConnectFailure = true RetryOnConnectFailure = true
RetryOnStatusCodes = [401, 209] RetryOnStatusCodes = [401, 209]
@ -992,6 +994,7 @@ func TestParseConfigEntry(t *testing.T) {
"partition": "chard", "partition": "chard",
"prefix_rewrite": "/alternate", "prefix_rewrite": "/alternate",
"request_timeout": "99s", "request_timeout": "99s",
"idle_timeout": "99s",
"num_retries": 12345, "num_retries": 12345,
"retry_on_connect_failure": true, "retry_on_connect_failure": true,
"retry_on_status_codes": [ "retry_on_status_codes": [
@ -1085,6 +1088,7 @@ func TestParseConfigEntry(t *testing.T) {
"Partition": "chard", "Partition": "chard",
"PrefixRewrite": "/alternate", "PrefixRewrite": "/alternate",
"RequestTimeout": "99s", "RequestTimeout": "99s",
"IdleTimeout": "99s",
"NumRetries": 12345, "NumRetries": 12345,
"RetryOnConnectFailure": true, "RetryOnConnectFailure": true,
"RetryOnStatusCodes": [ "RetryOnStatusCodes": [
@ -1177,6 +1181,7 @@ func TestParseConfigEntry(t *testing.T) {
Partition: "chard", Partition: "chard",
PrefixRewrite: "/alternate", PrefixRewrite: "/alternate",
RequestTimeout: 99 * time.Second, RequestTimeout: 99 * time.Second,
IdleTimeout: 99 * time.Second,
NumRetries: 12345, NumRetries: 12345,
RetryOnConnectFailure: true, RetryOnConnectFailure: true,
RetryOnStatusCodes: []uint32{401, 209}, RetryOnStatusCodes: []uint32{401, 209},

View File

@ -696,6 +696,12 @@ spec:
description: description:
'The total amount of time permitted for the entire downstream request (and retries) to be processed.', 'The total amount of time permitted for the entire downstream request (and retries) to be processed.',
}, },
{
name: 'IdleTimeout',
type: 'duration: 0',
description:
'The total amount of time permitted for the request stream to be idle',
},
{ {
name: 'NumRetries', name: 'NumRetries',
type: 'int: 0', type: 'int: 0',

View File

@ -330,6 +330,11 @@ defaults that are inherited by all services.
specified, inherits the Envoy default for route timeouts (15s). A value of 0 will specified, inherits the Envoy default for route timeouts (15s). A value of 0 will
disable request timeouts. disable request timeouts.
- `local_idle_timeout_ms` - In milliseconds, the idle timeout for HTTP requests
to the local application instance. Applies to HTTP based protocols only. If not
specified, inherits the Envoy default for route idle timeouts (15s). A value of 0
disables request timeouts.
- `max_inbound_connections` - The maximum number of concurrent inbound connections - `max_inbound_connections` - The maximum number of concurrent inbound connections
to the local application instance. If not specified, inherits the Envoy default (1024). to the local application instance. If not specified, inherits the Envoy default (1024).