Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions examples/using-http-connection-pool/README.md
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
39 changes: 39 additions & 0 deletions examples/using-http-connection-pool/main.go
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()
}
15 changes: 12 additions & 3 deletions examples/using-http-service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import (
func main() {
a := gofr.New()

// HTTP service with Circuit Breaker config given, uses custom health check
// either of circuit breaker or health can be used as well, as both implement service.Options interface.
// HTTP service with Circuit Breaker, Health Check, and Connection Pool configuration
// Note: /breeds is not an actual health check endpoint for "https://catfact.ninja"
a.AddHTTPService("cat-facts", "https://catfact.ninja",
&service.CircuitBreakerConfig{
Expand All @@ -23,13 +22,23 @@ func main() {
&service.HealthConfig{
HealthEndpoint: "breeds",
},
&service.ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
)

// service with improper health-check to test health check
// service with connection pool configuration for high-frequency requests
a.AddHTTPService("fact-checker", "https://catfact.ninja",
&service.HealthConfig{
HealthEndpoint: "breed",
},
&service.ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 15 * time.Second,
},
)

a.GET("/fact", Handler)
Expand Down
8 changes: 7 additions & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ cloud.google.com/go/compute v1.38.0 h1:MilCLYQW2m7Dku8hRIIKo4r0oKastlD74sSu16riY
cloud.google.com/go/compute v1.38.0/go.mod h1:oAFNIuXOmXbK/ssXm3z4nZB8ckPdjltJ7xhHCdbWFZM=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8=
cloud.google.com/go/container v1.43.0/go.mod h1:ETU9WZ1KM9ikEKLzrhRVao7KHtalDQu6aPqM34zDr/U=
cloud.google.com/go/containeranalysis v0.14.1/go.mod h1:28e+tlZgauWGHmEbnI5UfIsjMmrkoR1tFN0K2i71jBI=
Expand Down Expand Up @@ -292,6 +293,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down Expand Up @@ -321,6 +323,7 @@ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -369,8 +372,8 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
Expand Down Expand Up @@ -428,6 +431,7 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
Expand Down Expand Up @@ -497,6 +501,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250929231259-57b25ae835d4/go.mod h1:YUQUKndxDbAanQC0ln4pZ3Sis3N5sqgDte2XQqufkJc=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251022142026-3a174f9686a8/go.mod h1:ejCb7yLmK6GCVHp5qpeKbm4KZew/ldg+9b8kq5MONgk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
Expand All @@ -523,6 +528,7 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
38 changes: 38 additions & 0 deletions pkg/gofr/service/connection_pool.go
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 {
Copy link
Member

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.

// 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 {
Copy link
Member

Choose a reason for hiding this comment

The 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, h will be *circuitBreaker, not *httpService, and the type assertion fails.

// Create a custom transport with connection pool settings
transport := &http.Transport{
Copy link
Member

Choose a reason for hiding this comment

The 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 http.DefaultTransport.(*http.Transport).Clone() and then modify the connection pool settings.

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
}
87 changes: 87 additions & 0 deletions pkg/gofr/service/connection_pool_test.go
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)
}