Skip to content

Commit e2d9a7c

Browse files
authored
feat: Add new transport configuration options (#320)
Supports: - idleConnTimeout - maxIdleConns - maxIdleConnsPerHost - disableKeepAlives
1 parent 62f60a6 commit e2d9a7c

File tree

4 files changed

+280
-0
lines changed

4 files changed

+280
-0
lines changed

ldcomponents/http_configuration_builder.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ func (b *HTTPConfigurationBuilder) CACertFile(filePath string) *HTTPConfiguratio
8888
return b
8989
}
9090

91+
// HTTPOptions allows specifying additional HTTP transport options that will be applied to the
92+
// underlying HTTP transport used by the SDK.
93+
//
94+
// These options are applied in addition to any other HTTP configuration settings you may have
95+
// specified with other methods on this builder. The options are processed in the order they
96+
// are provided.
97+
func (b *HTTPConfigurationBuilder) HTTPOptions(opts []ldhttp.TransportOption) *HTTPConfigurationBuilder {
98+
if b.checkValid() {
99+
b.httpOptions = append(b.httpOptions, opts...)
100+
}
101+
102+
return b
103+
}
104+
91105
// ConnectTimeout sets the connection timeout.
92106
//
93107
// This is the maximum amount of time to wait for each individual connection attempt to a remote service

ldcomponents/http_configuration_builder_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/launchdarkly/go-server-sdk/v7/interfaces"
1414
"github.com/launchdarkly/go-server-sdk/v7/internal"
15+
"github.com/launchdarkly/go-server-sdk/v7/ldhttp"
1516
"github.com/launchdarkly/go-server-sdk/v7/subsystems"
1617

1718
helpers "github.com/launchdarkly/go-test-helpers/v3"
@@ -254,6 +255,119 @@ func TestHTTPConfigurationBuilder(t *testing.T) {
254255
})
255256
})
256257

258+
t.Run("HTTPOptions with IdleConnTimeout", func(t *testing.T) {
259+
customTimeout := 45 * time.Second
260+
c, err := HTTPConfiguration().
261+
HTTPOptions([]ldhttp.TransportOption{ldhttp.IdleConnTimeoutOption(customTimeout)}).
262+
Build(basicConfig)
263+
require.NoError(t, err)
264+
265+
client := c.CreateHTTPClient()
266+
require.NotNil(t, client.Transport)
267+
transport := client.Transport.(*http.Transport)
268+
assert.Equal(t, customTimeout, transport.IdleConnTimeout)
269+
})
270+
271+
t.Run("HTTPOptions with MaxIdleConns", func(t *testing.T) {
272+
customMax := 50
273+
c, err := HTTPConfiguration().
274+
HTTPOptions([]ldhttp.TransportOption{ldhttp.MaxIdleConnsOption(customMax)}).
275+
Build(basicConfig)
276+
require.NoError(t, err)
277+
278+
client := c.CreateHTTPClient()
279+
require.NotNil(t, client.Transport)
280+
transport := client.Transport.(*http.Transport)
281+
assert.Equal(t, customMax, transport.MaxIdleConns)
282+
})
283+
284+
t.Run("HTTPOptions with MaxIdleConnsPerHost", func(t *testing.T) {
285+
customMax := 15
286+
c, err := HTTPConfiguration().
287+
HTTPOptions([]ldhttp.TransportOption{ldhttp.MaxIdleConnsPerHostOption(customMax)}).
288+
Build(basicConfig)
289+
require.NoError(t, err)
290+
291+
client := c.CreateHTTPClient()
292+
require.NotNil(t, client.Transport)
293+
transport := client.Transport.(*http.Transport)
294+
assert.Equal(t, customMax, transport.MaxIdleConnsPerHost)
295+
})
296+
297+
t.Run("HTTPOptions with DisableKeepAlives", func(t *testing.T) {
298+
c, err := HTTPConfiguration().
299+
HTTPOptions([]ldhttp.TransportOption{ldhttp.DisableKeepAlivesOption(true)}).
300+
Build(basicConfig)
301+
require.NoError(t, err)
302+
303+
client := c.CreateHTTPClient()
304+
require.NotNil(t, client.Transport)
305+
transport := client.Transport.(*http.Transport)
306+
assert.True(t, transport.DisableKeepAlives)
307+
})
308+
309+
t.Run("HTTPOptions with multiple transport options", func(t *testing.T) {
310+
customIdleTimeout := 30 * time.Second
311+
customMaxIdle := 75
312+
customMaxIdlePerHost := 20
313+
314+
c, err := HTTPConfiguration().
315+
HTTPOptions([]ldhttp.TransportOption{
316+
ldhttp.IdleConnTimeoutOption(customIdleTimeout),
317+
ldhttp.MaxIdleConnsOption(customMaxIdle),
318+
ldhttp.MaxIdleConnsPerHostOption(customMaxIdlePerHost),
319+
ldhttp.DisableKeepAlivesOption(true),
320+
}).
321+
Build(basicConfig)
322+
require.NoError(t, err)
323+
324+
client := c.CreateHTTPClient()
325+
require.NotNil(t, client.Transport)
326+
transport := client.Transport.(*http.Transport)
327+
assert.Equal(t, customIdleTimeout, transport.IdleConnTimeout)
328+
assert.Equal(t, customMaxIdle, transport.MaxIdleConns)
329+
assert.Equal(t, customMaxIdlePerHost, transport.MaxIdleConnsPerHost)
330+
assert.True(t, transport.DisableKeepAlives)
331+
})
332+
333+
t.Run("HTTPOptions combined with other builder methods", func(t *testing.T) {
334+
customIdleTimeout := 60 * time.Second
335+
customConnectTimeout := 5 * time.Second
336+
337+
c, err := HTTPConfiguration().
338+
ConnectTimeout(customConnectTimeout).
339+
HTTPOptions([]ldhttp.TransportOption{ldhttp.IdleConnTimeoutOption(customIdleTimeout)}).
340+
Header("Custom-Header", "test-value").
341+
Build(basicConfig)
342+
require.NoError(t, err)
343+
344+
client := c.CreateHTTPClient()
345+
assert.Equal(t, customConnectTimeout, client.Timeout)
346+
347+
require.NotNil(t, client.Transport)
348+
transport := client.Transport.(*http.Transport)
349+
assert.Equal(t, customIdleTimeout, transport.IdleConnTimeout)
350+
351+
assert.Equal(t, "test-value", c.DefaultHeaders.Get("Custom-Header"))
352+
})
353+
354+
t.Run("HTTPOptions called multiple times accumulates options", func(t *testing.T) {
355+
customIdleTimeout := 25 * time.Second
356+
customMaxIdle := 80
357+
358+
c, err := HTTPConfiguration().
359+
HTTPOptions([]ldhttp.TransportOption{ldhttp.IdleConnTimeoutOption(customIdleTimeout)}).
360+
HTTPOptions([]ldhttp.TransportOption{ldhttp.MaxIdleConnsOption(customMaxIdle)}).
361+
Build(basicConfig)
362+
require.NoError(t, err)
363+
364+
client := c.CreateHTTPClient()
365+
require.NotNil(t, client.Transport)
366+
transport := client.Transport.(*http.Transport)
367+
assert.Equal(t, customIdleTimeout, transport.IdleConnTimeout)
368+
assert.Equal(t, customMaxIdle, transport.MaxIdleConns)
369+
})
370+
257371
t.Run("nil safety", func(t *testing.T) {
258372
var b *HTTPConfigurationBuilder = nil
259373
b = b.ConnectTimeout(0).Header("a", "b").ProxyURL("c").Wrapper("d", "e")

ldhttp/http_transport.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ type transportExtraOptions struct {
2222
caCerts *x509.CertPool
2323
connectTimeout time.Duration
2424
proxyURL *url.URL
25+
26+
// idleConnTimeout and maxIdleConns have to be pointers to support backwards
27+
// compatibility as we provide defaults for these.
28+
idleConnTimeout *time.Duration
29+
maxIdleConns *int
30+
31+
maxIdleConnsPerHost int
32+
disableKeepAlives bool
2533
}
2634

2735
// TransportOption is the interface for optional configuration parameters that can be passed to NewHTTPTransport.
@@ -101,6 +109,65 @@ func (o proxyOption) apply(opts *transportExtraOptions) error {
101109
return nil
102110
}
103111

112+
type idleConnTimeoutOption struct {
113+
timeout time.Duration
114+
}
115+
116+
func (o idleConnTimeoutOption) apply(opts *transportExtraOptions) error {
117+
opts.idleConnTimeout = &o.timeout
118+
return nil
119+
}
120+
121+
// IdleConnTimeoutOption specifies the maximum amount of time an idle (keep-alive) connection will remain
122+
// idle before closing itself, when used with NewHTTPTransport.
123+
func IdleConnTimeoutOption(timeout time.Duration) TransportOption {
124+
return idleConnTimeoutOption{timeout: timeout}
125+
}
126+
127+
type maxIdleConnsOption struct {
128+
count int
129+
}
130+
131+
func (o maxIdleConnsOption) apply(opts *transportExtraOptions) error {
132+
opts.maxIdleConns = &o.count
133+
return nil
134+
}
135+
136+
// MaxIdleConnsOption specifies the maximum number of idle (keep-alive) connections across all hosts,
137+
// when used with NewHTTPTransport.
138+
func MaxIdleConnsOption(count int) TransportOption {
139+
return maxIdleConnsOption{count: count}
140+
}
141+
142+
type maxIdleConnsPerHostOption struct {
143+
count int
144+
}
145+
146+
func (o maxIdleConnsPerHostOption) apply(opts *transportExtraOptions) error {
147+
opts.maxIdleConnsPerHost = o.count
148+
return nil
149+
}
150+
151+
// MaxIdleConnsPerHostOption specifies the maximum number of idle (keep-alive) connections per host,
152+
// when used with NewHTTPTransport.
153+
func MaxIdleConnsPerHostOption(count int) TransportOption {
154+
return maxIdleConnsPerHostOption{count: count}
155+
}
156+
157+
type disableKeepAlivesOption struct {
158+
disable bool
159+
}
160+
161+
func (o disableKeepAlivesOption) apply(opts *transportExtraOptions) error {
162+
opts.disableKeepAlives = o.disable
163+
return nil
164+
}
165+
166+
// DisableKeepAlivesOption disables HTTP keep-alives when set to true, when used with NewHTTPTransport.
167+
func DisableKeepAlivesOption(disable bool) TransportOption {
168+
return disableKeepAlivesOption{disable: disable}
169+
}
170+
104171
// NewHTTPTransport creates a customized http.Transport struct using the specified options. It returns both
105172
// the Transport and an associated net.Dialer.
106173
//
@@ -128,6 +195,18 @@ func NewHTTPTransport(options ...TransportOption) (*http.Transport, *net.Dialer,
128195
if extraOptions.proxyURL != nil {
129196
transport.Proxy = http.ProxyURL(extraOptions.proxyURL)
130197
}
198+
if extraOptions.idleConnTimeout != nil {
199+
transport.IdleConnTimeout = *extraOptions.idleConnTimeout
200+
}
201+
if extraOptions.maxIdleConns != nil {
202+
transport.MaxIdleConns = *extraOptions.maxIdleConns
203+
}
204+
if extraOptions.maxIdleConnsPerHost != 0 {
205+
transport.MaxIdleConnsPerHost = extraOptions.maxIdleConnsPerHost
206+
}
207+
if extraOptions.disableKeepAlives {
208+
transport.DisableKeepAlives = true
209+
}
131210
return transport, dialer, nil
132211
}
133212

ldhttp/http_transport_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"reflect"
1010
"testing"
11+
"time"
1112

1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
@@ -87,3 +88,75 @@ func TestCanSetProxyURL(t *testing.T) {
8788
require.NoError(t, err)
8889
assert.Equal(t, url, urlOut)
8990
}
91+
92+
func TestCanSetIdleConnTimeout(t *testing.T) {
93+
customTimeout := 30 * time.Second
94+
transport, _, err := NewHTTPTransport(IdleConnTimeoutOption(customTimeout))
95+
require.NoError(t, err)
96+
assert.Equal(t, customTimeout, transport.IdleConnTimeout)
97+
}
98+
99+
func TestDefaultIdleConnTimeoutIsUsedWhenNotSet(t *testing.T) {
100+
transport, _, err := NewHTTPTransport()
101+
require.NoError(t, err)
102+
// Should use the default from newDefaultTransport
103+
assert.Equal(t, 90*time.Second, transport.IdleConnTimeout)
104+
}
105+
106+
func TestCanSetMaxIdleConns(t *testing.T) {
107+
customMax := 50
108+
transport, _, err := NewHTTPTransport(MaxIdleConnsOption(customMax))
109+
require.NoError(t, err)
110+
assert.Equal(t, customMax, transport.MaxIdleConns)
111+
}
112+
113+
func TestDefaultMaxIdleConnsIsUsedWhenNotSet(t *testing.T) {
114+
transport, _, err := NewHTTPTransport()
115+
require.NoError(t, err)
116+
// Should use the default from newDefaultTransport
117+
assert.Equal(t, 100, transport.MaxIdleConns)
118+
}
119+
120+
func TestCanSetMaxIdleConnsPerHost(t *testing.T) {
121+
customMax := 10
122+
transport, _, err := NewHTTPTransport(MaxIdleConnsPerHostOption(customMax))
123+
require.NoError(t, err)
124+
assert.Equal(t, customMax, transport.MaxIdleConnsPerHost)
125+
}
126+
127+
func TestDefaultMaxIdleConnsPerHostIsUsedWhenNotSet(t *testing.T) {
128+
transport, _, err := NewHTTPTransport()
129+
require.NoError(t, err)
130+
// Should be 0 (no limit) when not set
131+
assert.Equal(t, 0, transport.MaxIdleConnsPerHost)
132+
}
133+
134+
func TestCanDisableKeepAlives(t *testing.T) {
135+
transport, _, err := NewHTTPTransport(DisableKeepAlivesOption(true))
136+
require.NoError(t, err)
137+
assert.True(t, transport.DisableKeepAlives)
138+
}
139+
140+
func TestKeepAlivesEnabledByDefault(t *testing.T) {
141+
transport, _, err := NewHTTPTransport()
142+
require.NoError(t, err)
143+
assert.False(t, transport.DisableKeepAlives)
144+
}
145+
146+
func TestCanSetMultipleConnectionOptions(t *testing.T) {
147+
customIdleTimeout := 45 * time.Second
148+
customMaxIdle := 75
149+
customMaxIdlePerHost := 15
150+
151+
transport, _, err := NewHTTPTransport(
152+
IdleConnTimeoutOption(customIdleTimeout),
153+
MaxIdleConnsOption(customMaxIdle),
154+
MaxIdleConnsPerHostOption(customMaxIdlePerHost),
155+
DisableKeepAlivesOption(true),
156+
)
157+
require.NoError(t, err)
158+
assert.Equal(t, customIdleTimeout, transport.IdleConnTimeout)
159+
assert.Equal(t, customMaxIdle, transport.MaxIdleConns)
160+
assert.Equal(t, customMaxIdlePerHost, transport.MaxIdleConnsPerHost)
161+
assert.True(t, transport.DisableKeepAlives)
162+
}

0 commit comments

Comments
 (0)