diff --git a/examples/using-http-connection-pool/README.md b/examples/using-http-connection-pool/README.md new file mode 100644 index 0000000000..6cfbcd6dda --- /dev/null +++ b/examples/using-http-connection-pool/README.md @@ -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 \ No newline at end of file diff --git a/examples/using-http-connection-pool/main.go b/examples/using-http-connection-pool/main.go new file mode 100644 index 0000000000..1127cedbf0 --- /dev/null +++ b/examples/using-http-connection-pool/main.go @@ -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() +} \ No newline at end of file diff --git a/examples/using-http-service/main.go b/examples/using-http-service/main.go index 1348389e74..100fd6cc8e 100644 --- a/examples/using-http-service/main.go +++ b/examples/using-http-service/main.go @@ -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{ @@ -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) diff --git a/go.work.sum b/go.work.sum index ef28a2ce18..2e9b304347 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/pkg/gofr/service/connection_pool.go b/pkg/gofr/service/connection_pool.go new file mode 100644 index 0000000000..dc1db67d40 --- /dev/null +++ b/pkg/gofr/service/connection_pool.go @@ -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 { + // Create a custom transport with connection pool settings + transport := &http.Transport{ + MaxIdleConns: c.MaxIdleConns, + MaxIdleConnsPerHost: c.MaxIdleConnsPerHost, + IdleConnTimeout: c.IdleConnTimeout, + } + + // Apply the custom transport to the HTTP client + httpSvc.Client.Transport = transport + } + + return h +} diff --git a/pkg/gofr/service/connection_pool_test.go b/pkg/gofr/service/connection_pool_test.go new file mode 100644 index 0000000000..fd54a8efdd --- /dev/null +++ b/pkg/gofr/service/connection_pool_test.go @@ -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) +}