diff --git a/config/local-dev.yaml b/config/local-dev.yaml index abe11f8..b645350 100644 --- a/config/local-dev.yaml +++ b/config/local-dev.yaml @@ -22,6 +22,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -65,6 +70,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -104,6 +114,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -143,6 +158,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry diff --git a/config/local-simple.yaml b/config/local-simple.yaml index e4a29f2..df903ce 100644 --- a/config/local-simple.yaml +++ b/config/local-simple.yaml @@ -22,6 +22,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -67,6 +72,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -108,6 +118,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -147,6 +162,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry diff --git a/config/onix-bap/adapter.yaml b/config/onix-bap/adapter.yaml index 598fc9b..d5f437c 100644 --- a/config/onix-bap/adapter.yaml +++ b/config/onix-bap/adapter.yaml @@ -23,6 +23,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -68,6 +73,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry diff --git a/config/onix-bpp/adapter.yaml b/config/onix-bpp/adapter.yaml index 6036752..1ca4064 100644 --- a/config/onix-bpp/adapter.yaml +++ b/config/onix-bpp/adapter.yaml @@ -24,6 +24,11 @@ modules: type: std role: bpp subscriberId: bpp1 + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -69,6 +74,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry diff --git a/config/onix/adapter.yaml b/config/onix/adapter.yaml index 20ccbab..dbd67e3 100644 --- a/config/onix/adapter.yaml +++ b/config/onix/adapter.yaml @@ -23,6 +23,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -68,6 +73,11 @@ modules: handler: type: std role: bap + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -114,6 +124,11 @@ modules: type: std role: bpp subscriberId: bpp1 + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry @@ -159,6 +174,11 @@ modules: handler: type: std role: bpp + httpClientConfig: + maxIdleConns: 1000 + maxIdleConnsPerHost: 200 + idleConnTimeout: 300s + responseHeaderTimeout: 5s plugins: registry: id: registry diff --git a/core/module/handler/config.go b/core/module/handler/config.go index 2de4476..7891a4d 100644 --- a/core/module/handler/config.go +++ b/core/module/handler/config.go @@ -3,6 +3,7 @@ package handler import ( "context" "net/http" + "time" "github.com/beckn-one/beckn-onix/pkg/model" "github.com/beckn-one/beckn-onix/pkg/plugin" @@ -46,12 +47,32 @@ type PluginCfg struct { Steps []plugin.Config } +// HttpClientConfig defines the configuration for the HTTP transport layer. +type HttpClientConfig struct { + // MaxIdleConns controls the maximum number of idle (keep-alive) + // connections across all hosts. + MaxIdleConns int `yaml:"maxIdleConns"` + + // IdleConnTimeout is the maximum amount of time an idle + // (keep-alive) connection will remain idle before closing itself. + IdleConnTimeout time.Duration `yaml:"idleConnTimeout"` + + // MaxIdleConnsPerHost, if non-zero, controls the maximum idle + // (keep-alive) connections to keep per-host. + MaxIdleConnsPerHost int `yaml:"maxIdleConnsPerHost"` + + // ResponseHeaderTimeout, if non-zero, specifies the amount of time to wait + // for a server's response headers after fully writing the request. + ResponseHeaderTimeout time.Duration `yaml:"responseHeaderTimeout"` +} + // Config holds the configuration for request processing handlers. type Config struct { - Plugins PluginCfg `yaml:"plugins"` - Steps []string - Type Type - RegistryURL string `yaml:"registryUrl"` - Role model.Role - SubscriberID string `yaml:"subscriberId"` + Plugins PluginCfg `yaml:"plugins"` + Steps []string + Type Type + RegistryURL string `yaml:"registryUrl"` + Role model.Role + SubscriberID string `yaml:"subscriberId"` + HttpClientConfig HttpClientConfig `yaml:"httpClientConfig"` } diff --git a/core/module/handler/stdHandler.go b/core/module/handler/stdHandler.go index c674abe..026b28f 100644 --- a/core/module/handler/stdHandler.go +++ b/core/module/handler/stdHandler.go @@ -28,6 +28,29 @@ type stdHandler struct { publisher definition.Publisher SubscriberID string role model.Role + httpClient *http.Client +} + +// newHTTPClient creates a new HTTP client with a custom transport configuration. +func newHTTPClient(cfg *HttpClientConfig) *http.Client { + // Clone the default transport to inherit its sensible defaults. + transport := http.DefaultTransport.(*http.Transport).Clone() + + // Only override the defaults if a value is explicitly provided in the config. + // A zero value in the config means we stick with the default values. + if cfg.MaxIdleConns > 0 { + transport.MaxIdleConns = cfg.MaxIdleConns + } + if cfg.MaxIdleConnsPerHost > 0 { + transport.MaxIdleConnsPerHost = cfg.MaxIdleConnsPerHost + } + if cfg.IdleConnTimeout > 0 { + transport.IdleConnTimeout = cfg.IdleConnTimeout + } + if cfg.ResponseHeaderTimeout > 0 { + transport.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout + } + return &http.Client{Transport: transport} } // NewStdHandler initializes a new processor with plugins and steps. @@ -36,6 +59,7 @@ func NewStdHandler(ctx context.Context, mgr PluginManager, cfg *Config) (http.Ha steps: []definition.Step{}, SubscriberID: cfg.SubscriberID, role: cfg.Role, + httpClient: newHTTPClient(&cfg.HttpClientConfig), } // Initialize plugins. if err := h.initPlugins(ctx, mgr, &cfg.Plugins); err != nil { @@ -74,7 +98,7 @@ func (h *stdHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Handle routing based on the defined route type. - route(ctx, r, w, h.publisher) + route(ctx, r, w, h.publisher, h.httpClient) } // stepCtx creates a new StepContext for processing an HTTP request. @@ -107,12 +131,12 @@ func (h *stdHandler) subID(ctx context.Context) string { var proxyFunc = proxy // route handles request forwarding or message publishing based on the routing type. -func route(ctx *model.StepContext, r *http.Request, w http.ResponseWriter, pb definition.Publisher) { +func route(ctx *model.StepContext, r *http.Request, w http.ResponseWriter, pb definition.Publisher, httpClient *http.Client) { log.Debugf(ctx, "Routing to ctx.Route to %#v", ctx.Route) switch ctx.Route.TargetType { case "url": log.Infof(ctx.Context, "Forwarding request to URL: %s", ctx.Route.URL) - proxyFunc(ctx, r, w) + proxyFunc(ctx, r, w, httpClient) return case "publisher": if pb == nil { @@ -136,7 +160,7 @@ func route(ctx *model.StepContext, r *http.Request, w http.ResponseWriter, pb de } response.SendAck(w) } -func proxy(ctx *model.StepContext, r *http.Request, w http.ResponseWriter) { +func proxy(ctx *model.StepContext, r *http.Request, w http.ResponseWriter, httpClient *http.Client) { target := ctx.Route.URL r.Header.Set("X-Forwarded-Host", r.Host) @@ -147,7 +171,10 @@ func proxy(ctx *model.StepContext, r *http.Request, w http.ResponseWriter) { log.Request(req.Context(), req, ctx.Body) } - proxy := &httputil.ReverseProxy{Director: director} + proxy := &httputil.ReverseProxy{ + Director: director, + Transport: httpClient.Transport, + } proxy.ServeHTTP(w, r) } diff --git a/core/module/handler/stdHandler_test.go b/core/module/handler/stdHandler_test.go new file mode 100644 index 0000000..bf65840 --- /dev/null +++ b/core/module/handler/stdHandler_test.go @@ -0,0 +1,153 @@ +package handler + +import ( + "net/http" + "testing" + "time" +) + +func TestNewHTTPClient(t *testing.T) { + tests := []struct { + name string + config HttpClientConfig + expected struct { + maxIdleConns int + maxIdleConnsPerHost int + idleConnTimeout time.Duration + responseHeaderTimeout time.Duration + } + }{ + { + name: "all values configured", + config: HttpClientConfig{ + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 200, + IdleConnTimeout: 300 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + }, + expected: struct { + maxIdleConns int + maxIdleConnsPerHost int + idleConnTimeout time.Duration + responseHeaderTimeout time.Duration + }{ + maxIdleConns: 1000, + maxIdleConnsPerHost: 200, + idleConnTimeout: 300 * time.Second, + responseHeaderTimeout: 5 * time.Second, + }, + }, + { + name: "zero values use defaults", + config: HttpClientConfig{}, + expected: struct { + maxIdleConns int + maxIdleConnsPerHost int + idleConnTimeout time.Duration + responseHeaderTimeout time.Duration + }{ + maxIdleConns: 100, // Go default + maxIdleConnsPerHost: 0, // Go default (unlimited per host) + idleConnTimeout: 90 * time.Second, + responseHeaderTimeout: 0, + }, + }, + { + name: "partial configuration", + config: HttpClientConfig{ + MaxIdleConns: 500, + IdleConnTimeout: 180 * time.Second, + }, + expected: struct { + maxIdleConns int + maxIdleConnsPerHost int + idleConnTimeout time.Duration + responseHeaderTimeout time.Duration + }{ + maxIdleConns: 500, + maxIdleConnsPerHost: 0, // Go default (unlimited per host) + idleConnTimeout: 180 * time.Second, + responseHeaderTimeout: 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := newHTTPClient(&tt.config) + + if client == nil { + t.Fatal("newHTTPClient returned nil") + } + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("client transport is not *http.Transport") + } + + if transport.MaxIdleConns != tt.expected.maxIdleConns { + t.Errorf("MaxIdleConns = %d, want %d", transport.MaxIdleConns, tt.expected.maxIdleConns) + } + + if transport.MaxIdleConnsPerHost != tt.expected.maxIdleConnsPerHost { + t.Errorf("MaxIdleConnsPerHost = %d, want %d", transport.MaxIdleConnsPerHost, tt.expected.maxIdleConnsPerHost) + } + + if transport.IdleConnTimeout != tt.expected.idleConnTimeout { + t.Errorf("IdleConnTimeout = %v, want %v", transport.IdleConnTimeout, tt.expected.idleConnTimeout) + } + + if transport.ResponseHeaderTimeout != tt.expected.responseHeaderTimeout { + t.Errorf("ResponseHeaderTimeout = %v, want %v", transport.ResponseHeaderTimeout, tt.expected.responseHeaderTimeout) + } + }) + } +} + +func TestHttpClientConfigDefaults(t *testing.T) { + // Test that zero config values don't override defaults + config := &HttpClientConfig{} + client := newHTTPClient(config) + + transport := client.Transport.(*http.Transport) + + // Verify defaults are preserved when config values are zero + if transport.MaxIdleConns == 0 { + t.Error("MaxIdleConns should not be zero when using defaults") + } + + // MaxIdleConnsPerHost default is 0 (unlimited), which is correct + if transport.MaxIdleConns != 100 { + t.Errorf("Expected default MaxIdleConns=100, got %d", transport.MaxIdleConns) + } +} + +func TestHttpClientConfigPerformanceValues(t *testing.T) { + // Test the specific performance-optimized values from the document + config := &HttpClientConfig{ + MaxIdleConns: 1000, + MaxIdleConnsPerHost: 200, + IdleConnTimeout: 300 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + } + + client := newHTTPClient(config) + transport := client.Transport.(*http.Transport) + + // Verify performance-optimized values + if transport.MaxIdleConns != 1000 { + t.Errorf("Expected MaxIdleConns=1000, got %d", transport.MaxIdleConns) + } + + if transport.MaxIdleConnsPerHost != 200 { + t.Errorf("Expected MaxIdleConnsPerHost=200, got %d", transport.MaxIdleConnsPerHost) + } + + if transport.IdleConnTimeout != 300*time.Second { + t.Errorf("Expected IdleConnTimeout=300s, got %v", transport.IdleConnTimeout) + } + + if transport.ResponseHeaderTimeout != 5*time.Second { + t.Errorf("Expected ResponseHeaderTimeout=5s, got %v", transport.ResponseHeaderTimeout) + } +} \ No newline at end of file