-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: add HTTP connection pool configuration support #2506
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Changes from all commits
4818234
65d25ec
0adeb43
17fae34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # HTTP Connection Pool Configuration Example | ||
|
|
||
| This example demonstrates how to configure HTTP connection pool settings for GoFr HTTP services to optimize performance for high-frequency requests. | ||
|
|
||
| ## Problem Solved | ||
|
|
||
| The default Go HTTP client has `MaxIdleConnsPerHost: 2`, which can cause: | ||
| - Connection pool exhaustion errors | ||
| - Increased latency (3x slower connection establishment) | ||
| - Poor connection reuse | ||
|
|
||
| ## Configuration Options | ||
|
|
||
| - **MaxIdleConns**: Maximum idle connections across all hosts | ||
| - **MaxIdleConnsPerHost**: Maximum idle connections per host (critical for performance) | ||
| - **IdleConnTimeout**: How long to keep idle connections alive | ||
|
|
||
| ## Running the Example | ||
|
|
||
| ```bash | ||
| go run main.go | ||
| ``` | ||
|
|
||
| Test the endpoint: | ||
| ```bash | ||
| curl http://localhost:8000/posts/1 | ||
| ``` | ||
|
|
||
| ## Benefits | ||
|
|
||
| - Eliminates connection pool exhaustion errors | ||
| - Improves performance for high-frequency inter-service calls | ||
| - Backward compatible with existing code |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "time" | ||
|
|
||
| "gofr.dev/pkg/gofr" | ||
| "gofr.dev/pkg/gofr/service" | ||
| ) | ||
|
|
||
| func main() { | ||
| app := gofr.New() | ||
|
|
||
| // HTTP service with optimized connection pool for high-frequency requests | ||
| app.AddHTTPService("api-service", "https://jsonplaceholder.typicode.com", | ||
| &service.ConnectionPoolConfig{ | ||
| MaxIdleConns: 100, // Maximum idle connections across all hosts | ||
| MaxIdleConnsPerHost: 20, // Maximum idle connections per host (increased from default 2) | ||
| IdleConnTimeout: 90 * time.Second, // Keep connections alive for 90 seconds | ||
| }, | ||
| ) | ||
|
|
||
| app.GET("/posts/{id}", func(ctx *gofr.Context) (any, error) { | ||
| id := ctx.PathParam("id") | ||
|
|
||
| svc := ctx.GetHTTPService("api-service") | ||
| resp, err := svc.Get(ctx, "posts/"+id, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| return map[string]any{ | ||
| "status": resp.Status, | ||
| "headers": resp.Header, | ||
| }, nil | ||
| }) | ||
|
|
||
| app.Run() | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package service | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "time" | ||
| ) | ||
|
|
||
| // ConnectionPoolConfig holds the configuration for HTTP connection pool settings. | ||
| type ConnectionPoolConfig struct { | ||
| // MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts. | ||
| // Zero means no limit. | ||
| MaxIdleConns int | ||
|
|
||
| // MaxIdleConnsPerHost controls the maximum idle (keep-alive) connections to keep per-host. | ||
| // If zero, DefaultMaxIdleConnsPerHost is used. | ||
| MaxIdleConnsPerHost int | ||
|
|
||
| // IdleConnTimeout is the maximum amount of time an idle (keep-alive) connection will remain | ||
| // idle before closing itself. Zero means no limit. | ||
| IdleConnTimeout time.Duration | ||
| } | ||
|
|
||
| // AddOption implements the Options interface to apply connection pool configuration to HTTP service. | ||
| func (c *ConnectionPoolConfig) AddOption(h HTTP) HTTP { | ||
| if httpSvc, ok := h.(*httpService); ok { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This type assertion might fail when other options (CircuitBreaker, Retry, OAuth) are applied first because they return wrapper types, not *httpService. The connection pool config will be silently ignored. For example, if CircuitBreakerConfig is applied first, |
||
| // Create a custom transport with connection pool settings | ||
| transport := &http.Transport{ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Creating a new transport loses important default settings like TLS timeouts and proxy configuration. Use Example: transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxIdleConns = c.MaxIdleConns
transport.MaxIdleConnsPerHost = c.MaxIdleConnsPerHost
transport.IdleConnTimeout = c.IdleConnTimeout |
||
| MaxIdleConns: c.MaxIdleConns, | ||
| MaxIdleConnsPerHost: c.MaxIdleConnsPerHost, | ||
| IdleConnTimeout: c.IdleConnTimeout, | ||
| } | ||
|
|
||
| // Apply the custom transport to the HTTP client | ||
| httpSvc.Client.Transport = transport | ||
| } | ||
|
|
||
| return h | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package service | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "go.uber.org/mock/gomock" | ||
| ) | ||
|
|
||
| func TestConnectionPoolConfig_AddOption(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| config *ConnectionPoolConfig | ||
| want *http.Transport | ||
| }{ | ||
| { | ||
| name: "custom connection pool settings", | ||
| config: &ConnectionPoolConfig{ | ||
| MaxIdleConns: 100, | ||
| MaxIdleConnsPerHost: 10, | ||
| IdleConnTimeout: 30 * time.Second, | ||
| }, | ||
| want: &http.Transport{ | ||
| MaxIdleConns: 100, | ||
| MaxIdleConnsPerHost: 10, | ||
| IdleConnTimeout: 30 * time.Second, | ||
| }, | ||
| }, | ||
| { | ||
| name: "zero values", | ||
| config: &ConnectionPoolConfig{ | ||
| MaxIdleConns: 0, | ||
| MaxIdleConnsPerHost: 0, | ||
| IdleConnTimeout: 0, | ||
| }, | ||
| want: &http.Transport{ | ||
| MaxIdleConns: 0, | ||
| MaxIdleConnsPerHost: 0, | ||
| IdleConnTimeout: 0, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Create a mock HTTP service | ||
| mockHTTPService := &httpService{ | ||
| Client: &http.Client{}, | ||
| } | ||
|
|
||
| // Apply the connection pool configuration | ||
| result := tt.config.AddOption(mockHTTPService) | ||
|
|
||
| // Verify the result is still the same service | ||
| assert.Equal(t, mockHTTPService, result) | ||
|
|
||
| // Verify the transport was configured correctly | ||
| transport, ok := mockHTTPService.Client.Transport.(*http.Transport) | ||
| assert.True(t, ok, "Transport should be of type *http.Transport") | ||
| assert.Equal(t, tt.want.MaxIdleConns, transport.MaxIdleConns) | ||
| assert.Equal(t, tt.want.MaxIdleConnsPerHost, transport.MaxIdleConnsPerHost) | ||
| assert.Equal(t, tt.want.IdleConnTimeout, transport.IdleConnTimeout) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestConnectionPoolConfig_AddOption_NonHTTPService(t *testing.T) { | ||
| ctrl := gomock.NewController(t) | ||
| defer ctrl.Finish() | ||
|
|
||
| config := &ConnectionPoolConfig{ | ||
| MaxIdleConns: 100, | ||
| MaxIdleConnsPerHost: 10, | ||
| IdleConnTimeout: 30 * time.Second, | ||
| } | ||
|
|
||
| // Create a mock service that's not an httpService | ||
| mockService := NewMockHTTP(ctrl) | ||
|
|
||
| // Apply the configuration | ||
| result := config.AddOption(mockService) | ||
|
|
||
| // Should return the same service unchanged | ||
| assert.Equal(t, mockService, result) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding validation for configuration values. Negative or zero values for MaxIdleConns/MaxIdleConnsPerHost might cause unexpected behavior. Document what happens with zero values.