Skip to content

Commit ba2976d

Browse files
hagen1778valyala
authored andcommitted
Allow to configure server's read/write/idle timeouts (#36)
* allow to configure server's read/write/idle timeouts * rm unused func * specify where timeout should be applied * add tests to check TimeoutCfg calculation
1 parent ba8e195 commit ba2976d

File tree

11 files changed

+284
-84
lines changed

11 files changed

+284
-84
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,19 @@ server:
430430
# By default requests are accepted from all the IPs.
431431
allowed_networks: ["office", "reporting-apps", "1.2.3.4"]
432432

433+
# ReadTimeout is the maximum duration for proxy to reading the entire
434+
# request, including the body.
435+
# Default value is 1m
436+
read_timeout: 5m
437+
438+
# WriteTimeout is the maximum duration for proxy before timing out writes of the response.
439+
# Default is largest MaxExecutionTime + MaxQueueTime value from Users or Clusters
440+
write_timeout: 10m
441+
442+
# IdleTimeout is the maximum amount of time for proxy to wait for the next request.
443+
# Default is 10m
444+
idle_timeout: 20m
445+
433446
# Configs for input https interface.
434447
# The interface works only if this section is present.
435448
https:

config/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ listen_addr: <addr>
106106
# List of networks or network_groups access is allowed from
107107
# Each list item could be IP address or subnet mask
108108
allowed_networks: <network_groups>, <networks> ... | optional
109+
110+
# ReadTimeout is the maximum duration for reading the entire
111+
# request, including the body.
112+
read_timeout: <duration> | optional | default = 1m
113+
114+
# WriteTimeout is the maximum duration before timing out writes of the response.
115+
# Default is largest MaxExecutionTime + MaxQueueTime value from Users or Clusters
116+
write_timeout: <duration> | optional
117+
118+
// IdleTimeout is the maximum amount of time to wait for the next request.
119+
idle_timeout: <duration> | optional | default = 10m
109120
```
110121
111122
### <https_config>
@@ -117,6 +128,17 @@ listen_addr: <addr> | optional | default = `:443`
117128
# Each list item could be IP address or subnet mask
118129
allowed_networks: <network_groups>, <networks> ... | optional
119130

131+
# ReadTimeout is the maximum duration for proxy to reading the entire
132+
# request, including the body.
133+
read_timeout: <duration> | optional | default = 1m
134+
135+
# WriteTimeout is the maximum duration for proxy before timing out writes of the response.
136+
# Default is largest MaxExecutionTime + MaxQueueTime value from Users or Clusters
137+
write_timeout: <duration> | optional
138+
139+
// IdleTimeout is the maximum amount of time for proxy to wait for the next request.
140+
idle_timeout: <duration> | optional | default = 10m
141+
120142
# Certificate and key files for client cert authentication to the server
121143
cert_file: <string> | optional
122144
key_file: <string> | optional

config/config.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ func (s *Server) UnmarshalYAML(unmarshal func(interface{}) error) error {
113113
return checkOverflow(s.XXX, "server")
114114
}
115115

116+
// TimeoutCfg contains configurable http.Server timeouts
117+
type TimeoutCfg struct {
118+
// ReadTimeout is the maximum duration for reading the entire
119+
// request, including the body.
120+
// Default value is 1m
121+
ReadTimeout Duration `yaml:"read_timeout,omitempty"`
122+
123+
// WriteTimeout is the maximum duration before timing out writes of the response.
124+
// Default is largest MaxExecutionTime + MaxQueueTime value from Users or Clusters
125+
WriteTimeout Duration `yaml:"write_timeout,omitempty"`
126+
127+
// IdleTimeout is the maximum amount of time to wait for the next request.
128+
// Default is 10m
129+
IdleTimeout Duration `yaml:"idle_timeout,omitempty"`
130+
}
131+
116132
// HTTP describes configuration for server to listen HTTP connections
117133
type HTTP struct {
118134
// TCP address to listen to for http
@@ -128,6 +144,8 @@ type HTTP struct {
128144
// Whether to support Autocert handler for http-01 challenge
129145
ForceAutocertHandler bool
130146

147+
TimeoutCfg `yaml:",inline"`
148+
131149
// Catches all undefined fields and must be empty after parsing.
132150
XXX map[string]interface{} `yaml:",inline"`
133151
}
@@ -138,6 +156,12 @@ func (c *HTTP) UnmarshalYAML(unmarshal func(interface{}) error) error {
138156
if err := unmarshal((*plain)(c)); err != nil {
139157
return err
140158
}
159+
if c.ReadTimeout == 0 {
160+
c.ReadTimeout = Duration(time.Minute)
161+
}
162+
if c.IdleTimeout == 0 {
163+
c.IdleTimeout = Duration(time.Minute * 10)
164+
}
141165
return checkOverflow(c.XXX, "http")
142166
}
143167

@@ -162,6 +186,8 @@ type HTTPS struct {
162186
// if omitted or zero - no limits would be applied
163187
AllowedNetworks Networks `yaml:"-"`
164188

189+
TimeoutCfg `yaml:",inline"`
190+
165191
// Catches all undefined fields and must be empty after parsing.
166192
XXX map[string]interface{} `yaml:",inline"`
167193
}
@@ -172,6 +198,12 @@ func (c *HTTPS) UnmarshalYAML(unmarshal func(interface{}) error) error {
172198
if err := unmarshal((*plain)(c)); err != nil {
173199
return err
174200
}
201+
if c.ReadTimeout == 0 {
202+
c.ReadTimeout = Duration(time.Minute)
203+
}
204+
if c.IdleTimeout == 0 {
205+
c.IdleTimeout = Duration(time.Minute * 10)
206+
}
175207
if len(c.ListenAddr) == 0 {
176208
c.ListenAddr = ":443"
177209
}
@@ -646,21 +678,45 @@ func LoadFile(filename string) (*Config, error) {
646678
if cfg.Server.Metrics.AllowedNetworks, err = cfg.groupToNetwork(cfg.Server.Metrics.NetworksOrGroups); err != nil {
647679
return nil, err
648680
}
681+
var maxResponseTime time.Duration
649682
for i := range cfg.Clusters {
650683
c := &cfg.Clusters[i]
651684
for j := range c.ClusterUsers {
652685
u := &c.ClusterUsers[j]
686+
cud := time.Duration(u.MaxExecutionTime + u.MaxQueueTime)
687+
if cud > maxResponseTime {
688+
maxResponseTime = cud
689+
}
653690
if u.AllowedNetworks, err = cfg.groupToNetwork(u.NetworksOrGroups); err != nil {
654691
return nil, err
655692
}
656693
}
657694
}
658695
for i := range cfg.Users {
659696
u := &cfg.Users[i]
697+
ud := time.Duration(u.MaxExecutionTime + u.MaxQueueTime)
698+
if ud > maxResponseTime {
699+
maxResponseTime = ud
700+
}
660701
if u.AllowedNetworks, err = cfg.groupToNetwork(u.NetworksOrGroups); err != nil {
661702
return nil, err
662703
}
663704
}
705+
706+
if maxResponseTime < 0 {
707+
maxResponseTime = 0
708+
}
709+
// Give an additional minute for the maximum response time,
710+
// so the response body may be sent to the requester.
711+
maxResponseTime += time.Minute
712+
if len(cfg.Server.HTTP.ListenAddr) > 0 && cfg.Server.HTTP.WriteTimeout == 0 {
713+
cfg.Server.HTTP.WriteTimeout = Duration(maxResponseTime)
714+
}
715+
716+
if len(cfg.Server.HTTPS.ListenAddr) > 0 && cfg.Server.HTTPS.WriteTimeout == 0 {
717+
cfg.Server.HTTPS.WriteTimeout = Duration(maxResponseTime)
718+
}
719+
664720
if err := cfg.checkVulnerabilities(); err != nil {
665721
return nil, fmt.Errorf("security breach: %s\nSet option `hack_me_please=true` to disable security errors", err)
666722
}

config/config_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,23 @@ func TestLoadConfig(t *testing.T) {
3939
ListenAddr: ":9090",
4040
NetworksOrGroups: []string{"office", "reporting-apps", "1.2.3.4"},
4141
ForceAutocertHandler: true,
42+
TimeoutCfg: TimeoutCfg{
43+
ReadTimeout: Duration(5 * time.Minute),
44+
WriteTimeout: Duration(10 * time.Minute),
45+
IdleTimeout: Duration(20 * time.Minute),
46+
},
4247
},
4348
HTTPS: HTTPS{
4449
ListenAddr: ":443",
4550
Autocert: Autocert{
4651
CacheDir: "certs_dir",
4752
AllowedHosts: []string{"example.com"},
4853
},
54+
TimeoutCfg: TimeoutCfg{
55+
ReadTimeout: Duration(time.Minute),
56+
WriteTimeout: Duration(140 * time.Second),
57+
IdleTimeout: Duration(10 * time.Minute),
58+
},
4959
},
5060
Metrics: Metrics{
5161
NetworksOrGroups: []string{"office"},
@@ -196,6 +206,11 @@ func TestLoadConfig(t *testing.T) {
196206
HTTP: HTTP{
197207
ListenAddr: ":8080",
198208
NetworksOrGroups: []string{"127.0.0.1"},
209+
TimeoutCfg: TimeoutCfg{
210+
ReadTimeout: Duration(time.Minute),
211+
WriteTimeout: Duration(time.Minute),
212+
IdleTimeout: Duration(10 * time.Minute),
213+
},
199214
},
200215
},
201216
Clusters: []Cluster{
@@ -518,3 +533,79 @@ func TestParseDurationNegative(t *testing.T) {
518533
}
519534
}
520535
}
536+
537+
func TestConfigTimeouts(t *testing.T) {
538+
var testCases = []struct {
539+
name string
540+
file string
541+
expectedCfg TimeoutCfg
542+
}{
543+
{
544+
"default",
545+
"testdata/default_values.yml",
546+
TimeoutCfg{
547+
ReadTimeout: Duration(time.Minute),
548+
WriteTimeout: Duration(time.Minute),
549+
IdleTimeout: Duration(10 * time.Minute),
550+
},
551+
},
552+
{
553+
"defined",
554+
"testdata/timeouts.defined.yml",
555+
TimeoutCfg{
556+
ReadTimeout: Duration(time.Minute),
557+
WriteTimeout: Duration(time.Hour),
558+
IdleTimeout: Duration(24 * time.Hour),
559+
},
560+
},
561+
{
562+
"calculated write 1",
563+
"testdata/timeouts.write.calculated.yml",
564+
TimeoutCfg{
565+
ReadTimeout: Duration(time.Minute),
566+
// 10 + 1 minute
567+
WriteTimeout: Duration(11 * 60 * time.Second),
568+
IdleTimeout: Duration(10 * time.Minute),
569+
},
570+
},
571+
{
572+
"calculated write 2",
573+
"testdata/timeouts.write.calculated2.yml",
574+
TimeoutCfg{
575+
ReadTimeout: Duration(time.Minute),
576+
// 20 + 1 minute
577+
WriteTimeout: Duration(21 * 60 * time.Second),
578+
IdleTimeout: Duration(10 * time.Minute),
579+
},
580+
},
581+
{
582+
"calculated write 3",
583+
"testdata/timeouts.write.calculated3.yml",
584+
TimeoutCfg{
585+
ReadTimeout: Duration(time.Minute),
586+
// 50 + 1 minute
587+
WriteTimeout: Duration(51 * 60 * time.Second),
588+
IdleTimeout: Duration(10 * time.Minute),
589+
},
590+
},
591+
}
592+
593+
for _, tc := range testCases {
594+
t.Run(tc.name, func(t *testing.T) {
595+
cfg, err := LoadFile(tc.file)
596+
if err != nil {
597+
t.Fatalf("unexpected error: %s", err)
598+
}
599+
got := cfg.Server.HTTP.TimeoutCfg
600+
if got.ReadTimeout != tc.expectedCfg.ReadTimeout {
601+
t.Fatalf("got ReadTimeout %v; expected to have: %v", got.ReadTimeout, tc.expectedCfg.ReadTimeout)
602+
}
603+
if got.WriteTimeout != tc.expectedCfg.WriteTimeout {
604+
t.Fatalf("got WriteTimeout %v; expected to have: %v", got.WriteTimeout, tc.expectedCfg.WriteTimeout)
605+
}
606+
if got.IdleTimeout != tc.expectedCfg.IdleTimeout {
607+
t.Fatalf("got IdleTimeout %v; expected to have: %v", got.IdleTimeout, tc.expectedCfg.IdleTimeout)
608+
}
609+
})
610+
}
611+
}

config/testdata/full.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ server:
9090
# By default requests are accepted from all the IPs.
9191
allowed_networks: ["office", "reporting-apps", "1.2.3.4"]
9292

93+
# ReadTimeout is the maximum duration for proxy to reading the entire
94+
# request, including the body.
95+
# Default value is 1m
96+
read_timeout: 5m
97+
98+
# WriteTimeout is the maximum duration for proxy before timing out writes of the response.
99+
# Default is largest MaxExecutionTime + MaxQueueTime value from Users or Clusters
100+
write_timeout: 10m
101+
102+
# IdleTimeout is the maximum amount of time for proxy to wait for the next request.
103+
# Default is 10m
104+
idle_timeout: 20m
105+
93106
# Configs for input https interface.
94107
# The interface works only if this section is present.
95108
https:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
hack_me_please: true
2+
server:
3+
http:
4+
listen_addr: ":8080"
5+
read_timeout: 1m
6+
write_timeout: 1h
7+
idle_timeout: 1d
8+
9+
users:
10+
- name: "default"
11+
to_cluster: "cluster"
12+
to_user: "default"
13+
14+
clusters:
15+
- name: "cluster"
16+
nodes: ["127.0.0.1:8123"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
hack_me_please: true
2+
server:
3+
http:
4+
listen_addr: ":8080"
5+
6+
users:
7+
- name: "default"
8+
to_cluster: "cluster"
9+
to_user: "default"
10+
max_execution_time: 5m
11+
12+
clusters:
13+
- name: "cluster"
14+
nodes: ["127.0.0.1:8123"]
15+
users:
16+
- name: "web"
17+
max_execution_time: 10m
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
hack_me_please: true
2+
server:
3+
http:
4+
listen_addr: ":8080"
5+
6+
users:
7+
- name: "default"
8+
to_cluster: "cluster"
9+
to_user: "default"
10+
max_execution_time: 5m
11+
- name: "default2"
12+
to_cluster: "cluster"
13+
to_user: "default"
14+
max_execution_time: 20m
15+
16+
clusters:
17+
- name: "cluster"
18+
nodes: ["127.0.0.1:8123"]
19+
users:
20+
- name: "web"
21+
max_execution_time: 10m
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
hack_me_please: true
2+
server:
3+
http:
4+
listen_addr: ":8080"
5+
6+
users:
7+
- name: "default"
8+
to_cluster: "cluster"
9+
to_user: "default"
10+
max_execution_time: 5m
11+
- name: "default2"
12+
to_cluster: "cluster"
13+
to_user: "default"
14+
max_execution_time: 20m
15+
16+
clusters:
17+
- name: "cluster"
18+
nodes: ["127.0.0.1:8123"]
19+
users:
20+
- name: "web"
21+
max_execution_time: 10m
22+
- name: "web2"
23+
max_execution_time: 50m

0 commit comments

Comments
 (0)