From e0630743c332ab8f4141176a628c9e7d6b94f555 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Tue, 21 Oct 2025 10:11:11 +0100 Subject: [PATCH 01/15] Add ReusePort config to confighttp This adds in a new field, `ReusePort` that, if set, sets the SO_REUSEPORT socket option on the listener port. If we're on non unix, this errors out instead as AFAIK SO_REUSEPORT isn't available. Cursory testing says that SO_REUSEADDR _might_ work, but I don't have a windows machine to test on. Signed-off-by: sinkingpoint --- .chloggen/so-reuse-port.yaml | 26 +++++++++++ config/confighttp/go.mod | 2 +- config/confighttp/listen_config_other.go | 30 ++++++++++++ config/confighttp/listen_config_windows.go | 18 ++++++++ config/confighttp/server.go | 13 +++++- config/confighttp/server_test.go | 54 ++++++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 .chloggen/so-reuse-port.yaml create mode 100644 config/confighttp/listen_config_other.go create mode 100644 config/confighttp/listen_config_windows.go diff --git a/.chloggen/so-reuse-port.yaml b/.chloggen/so-reuse-port.yaml new file mode 100644 index 000000000000..4b66241a50ab --- /dev/null +++ b/.chloggen/so-reuse-port.yaml @@ -0,0 +1,26 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: pkg/config/confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +# cspell:ignore REUSEPORT +note: Added ReusePort option to confighttp.ServerConfig to enable SO_REUSEPORT on supported platforms. + +# One or more tracking issues or pull requests related to the change +issues: [14046] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index d9c8d62d71f0..0add36ebd903 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -66,7 +66,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.35.0 golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/config/confighttp/listen_config_other.go b/config/confighttp/listen_config_other.go new file mode 100644 index 000000000000..9776ae1825d7 --- /dev/null +++ b/config/confighttp/listen_config_other.go @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +//go:build !windows + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { + cfg := net.ListenConfig{} + if sc.ReusePort { + cfg.Control = func(_, _ string, c syscall.RawConn) error { + var controlErr error + err := c.Control(func(fd uintptr) { + controlErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + if err != nil { + return err + } + return controlErr + } + } + + return cfg, nil +} diff --git a/config/confighttp/listen_config_windows.go b/config/confighttp/listen_config_windows.go new file mode 100644 index 000000000000..2a0a62ce04b9 --- /dev/null +++ b/config/confighttp/listen_config_windows.go @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +//go:build windows + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "errors" + "net" +) + +func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { + if sc.ReusePort { + return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") + } + + return net.ListenConfig{}, nil +} diff --git a/config/confighttp/server.go b/config/confighttp/server.go index f9da9a537682..502ae82837b6 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -96,6 +96,12 @@ type ServerConfig struct { // KeepAlivesEnabled controls whether HTTP keep-alives are enabled. // By default, keep-alives are always enabled. Only very resource-constrained environments should disable them. KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"` + + // ReusePort enables the SO_REUSEPORT socket option on the listener. + // This allows multiple server instances to bind to the same address/port. + // This is useful for horizontal scaling and zero-downtime restarts. + // Note: This option is not supported on all operating systems. + ReusePort bool `mapstructure:"reuse_port,omitempty"` } // NewDefaultServerConfig returns ServerConfig type object with default values. @@ -123,7 +129,12 @@ type AuthConfig struct { // ToListener creates a net.Listener. func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { - listener, err := net.Listen("tcp", sc.Endpoint) + cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations + if err != nil { + return nil, err + } + + listener, err := cfg.Listen(ctx, "tcp", sc.Endpoint) if err != nil { return nil, err } diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index b254efc21689..1931baedf655 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -12,6 +12,7 @@ import ( "net/http" "net/http/httptest" "path/filepath" + "runtime" "strconv" "strings" "testing" @@ -1169,3 +1170,56 @@ func TestServerMiddlewaresFieldCompatibility(t *testing.T) { assert.Len(t, serverConfig.Middlewares, 1) assert.Equal(t, component.MustNewID("server_middleware"), serverConfig.Middlewares[0].ID) } + +func TestServerReusePort(t *testing.T) { + if runtime.GOOS == "windows" { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: true, + } + + _, err := sc.ToListener(t.Context()) + require.Error(t, err, "ReusePort is not supported on Windows") + } + + tests := []struct { + name string + reusePort bool + expectedError bool + }{ + { + name: "ReusePort enabled", + reusePort: true, + expectedError: false, + }, + { + name: "ReusePort disabled", + reusePort: false, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: tt.reusePort, + } + + ln1, err := sc.ToListener(t.Context()) + require.NoError(t, err) + defer ln1.Close() + + ln2, err := sc.ToListener(t.Context()) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if ln2 != nil { + ln2.Close() + } + }) + } +} From 4285bb8c8281b0d74f7ec280f8643ef524634c96 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Wed, 22 Oct 2025 14:57:03 +0100 Subject: [PATCH 02/15] ci fix Signed-off-by: sinkingpoint --- config/confighttp/server_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 1931baedf655..9382569d57d5 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -1180,6 +1180,7 @@ func TestServerReusePort(t *testing.T) { _, err := sc.ToListener(t.Context()) require.Error(t, err, "ReusePort is not supported on Windows") + return } tests := []struct { From 1073d9753cc03392cecd73b3f14c509708349fb3 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Tue, 28 Oct 2025 09:22:41 +0000 Subject: [PATCH 03/15] Add Validate method to ServerConfig This adds a new Validate method that checks the getListenConfig call so that the new SO_REUSEPORT stuff fails in dry run on Windows Signed-off-by: sinkingpoint --- config/confighttp/server.go | 9 +++++++++ config/confighttp/server_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/config/confighttp/server.go b/config/confighttp/server.go index 502ae82837b6..66a9652a8c0a 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -127,6 +127,15 @@ type AuthConfig struct { _ struct{} } +func (sc *ServerConfig) Validate() error { + _, err := sc.getListenConfig() + if err != nil { + return err + } + + return nil +} + // ToListener creates a net.Listener. func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 9382569d57d5..756c70ba5644 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -1224,3 +1224,16 @@ func TestServerReusePort(t *testing.T) { }) } } + +func TestServerConfigValidate(t *testing.T) { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: true, + } + + if runtime.GOOS == "windows" { + require.Error(t, sc.Validate()) + } else { + require.NoError(t, sc.Validate()) + } +} From 77faf7e57a630fcd207bdcb23ef6c807f4e7c2fb Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Tue, 21 Oct 2025 10:11:11 +0100 Subject: [PATCH 04/15] Add ReusePort config to confighttp This adds in a new field, `ReusePort` that, if set, sets the SO_REUSEPORT socket option on the listener port. If we're on non unix, this errors out instead as AFAIK SO_REUSEPORT isn't available. Cursory testing says that SO_REUSEADDR _might_ work, but I don't have a windows machine to test on. Signed-off-by: sinkingpoint --- .chloggen/so-reuse-port.yaml | 26 +++++++++++ config/confighttp/go.mod | 2 +- config/confighttp/listen_config_other.go | 30 ++++++++++++ config/confighttp/listen_config_windows.go | 18 ++++++++ config/confighttp/server.go | 13 +++++- config/confighttp/server_test.go | 54 ++++++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 .chloggen/so-reuse-port.yaml create mode 100644 config/confighttp/listen_config_other.go create mode 100644 config/confighttp/listen_config_windows.go diff --git a/.chloggen/so-reuse-port.yaml b/.chloggen/so-reuse-port.yaml new file mode 100644 index 000000000000..4b66241a50ab --- /dev/null +++ b/.chloggen/so-reuse-port.yaml @@ -0,0 +1,26 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) +component: pkg/config/confighttp + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +# cspell:ignore REUSEPORT +note: Added ReusePort option to confighttp.ServerConfig to enable SO_REUSEPORT on supported platforms. + +# One or more tracking issues or pull requests related to the change +issues: [14046] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index 386153a67ab9..5be6f463af81 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -62,7 +62,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.41.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.35.0 golang.org/x/text v0.28.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect google.golang.org/grpc v1.76.0 // indirect diff --git a/config/confighttp/listen_config_other.go b/config/confighttp/listen_config_other.go new file mode 100644 index 000000000000..9776ae1825d7 --- /dev/null +++ b/config/confighttp/listen_config_other.go @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +//go:build !windows + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { + cfg := net.ListenConfig{} + if sc.ReusePort { + cfg.Control = func(_, _ string, c syscall.RawConn) error { + var controlErr error + err := c.Control(func(fd uintptr) { + controlErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + if err != nil { + return err + } + return controlErr + } + } + + return cfg, nil +} diff --git a/config/confighttp/listen_config_windows.go b/config/confighttp/listen_config_windows.go new file mode 100644 index 000000000000..2a0a62ce04b9 --- /dev/null +++ b/config/confighttp/listen_config_windows.go @@ -0,0 +1,18 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +//go:build windows + +package confighttp // import "go.opentelemetry.io/collector/config/confighttp" + +import ( + "errors" + "net" +) + +func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { + if sc.ReusePort { + return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") + } + + return net.ListenConfig{}, nil +} diff --git a/config/confighttp/server.go b/config/confighttp/server.go index f9da9a537682..502ae82837b6 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -96,6 +96,12 @@ type ServerConfig struct { // KeepAlivesEnabled controls whether HTTP keep-alives are enabled. // By default, keep-alives are always enabled. Only very resource-constrained environments should disable them. KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"` + + // ReusePort enables the SO_REUSEPORT socket option on the listener. + // This allows multiple server instances to bind to the same address/port. + // This is useful for horizontal scaling and zero-downtime restarts. + // Note: This option is not supported on all operating systems. + ReusePort bool `mapstructure:"reuse_port,omitempty"` } // NewDefaultServerConfig returns ServerConfig type object with default values. @@ -123,7 +129,12 @@ type AuthConfig struct { // ToListener creates a net.Listener. func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { - listener, err := net.Listen("tcp", sc.Endpoint) + cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations + if err != nil { + return nil, err + } + + listener, err := cfg.Listen(ctx, "tcp", sc.Endpoint) if err != nil { return nil, err } diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index b254efc21689..1931baedf655 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -12,6 +12,7 @@ import ( "net/http" "net/http/httptest" "path/filepath" + "runtime" "strconv" "strings" "testing" @@ -1169,3 +1170,56 @@ func TestServerMiddlewaresFieldCompatibility(t *testing.T) { assert.Len(t, serverConfig.Middlewares, 1) assert.Equal(t, component.MustNewID("server_middleware"), serverConfig.Middlewares[0].ID) } + +func TestServerReusePort(t *testing.T) { + if runtime.GOOS == "windows" { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: true, + } + + _, err := sc.ToListener(t.Context()) + require.Error(t, err, "ReusePort is not supported on Windows") + } + + tests := []struct { + name string + reusePort bool + expectedError bool + }{ + { + name: "ReusePort enabled", + reusePort: true, + expectedError: false, + }, + { + name: "ReusePort disabled", + reusePort: false, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: tt.reusePort, + } + + ln1, err := sc.ToListener(t.Context()) + require.NoError(t, err) + defer ln1.Close() + + ln2, err := sc.ToListener(t.Context()) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if ln2 != nil { + ln2.Close() + } + }) + } +} From d713c54bc2a5be7bbd60d774b8bcd449a3e877c9 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Wed, 22 Oct 2025 14:57:03 +0100 Subject: [PATCH 05/15] ci fix Signed-off-by: sinkingpoint --- config/confighttp/server_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 1931baedf655..9382569d57d5 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -1180,6 +1180,7 @@ func TestServerReusePort(t *testing.T) { _, err := sc.ToListener(t.Context()) require.Error(t, err, "ReusePort is not supported on Windows") + return } tests := []struct { From 6a5a94cc5b66ff4debf2aaf6ebc47b1d9dc8ff9a Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Tue, 28 Oct 2025 09:22:41 +0000 Subject: [PATCH 06/15] Add Validate method to ServerConfig This adds a new Validate method that checks the getListenConfig call so that the new SO_REUSEPORT stuff fails in dry run on Windows Signed-off-by: sinkingpoint --- config/confighttp/server.go | 9 +++++++++ config/confighttp/server_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/config/confighttp/server.go b/config/confighttp/server.go index 502ae82837b6..66a9652a8c0a 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -127,6 +127,15 @@ type AuthConfig struct { _ struct{} } +func (sc *ServerConfig) Validate() error { + _, err := sc.getListenConfig() + if err != nil { + return err + } + + return nil +} + // ToListener creates a net.Listener. func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 9382569d57d5..756c70ba5644 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -1224,3 +1224,16 @@ func TestServerReusePort(t *testing.T) { }) } } + +func TestServerConfigValidate(t *testing.T) { + sc := &ServerConfig{ + Endpoint: "localhost:4318", + ReusePort: true, + } + + if runtime.GOOS == "windows" { + require.Error(t, sc.Validate()) + } else { + require.NoError(t, sc.Validate()) + } +} From 92aec79e7585e79cf14c731c36972607fc9eb8ed Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Mon, 5 Jan 2026 11:32:47 +0000 Subject: [PATCH 07/15] Move ReusePort to confignet Rather than having this in confighttp specifically, this allows us to reuse it in the net AddrConfig stuff Signed-off-by: sinkingpoint --- config/confighttp/go.mod | 2 +- config/confighttp/server.go | 22 +----- config/confighttp/server_test.go | 68 ------------------ config/confignet/confignet.go | 12 +++- config/confignet/confignet_test.go | 71 +++++++++++++++++++ config/confignet/go.mod | 1 + config/confignet/go.sum | 2 + .../listen_config_other.go | 4 +- .../listen_config_windows.go | 4 +- 9 files changed, 91 insertions(+), 95 deletions(-) rename config/{confighttp => confignet}/listen_config_other.go (77%) rename config/{confighttp => confignet}/listen_config_windows.go (64%) diff --git a/config/confighttp/go.mod b/config/confighttp/go.mod index bf898d745086..844feeec6908 100644 --- a/config/confighttp/go.mod +++ b/config/confighttp/go.mod @@ -65,7 +65,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.46.0 // indirect - golang.org/x/sys v0.39.0 + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect google.golang.org/grpc v1.78.0 // indirect diff --git a/config/confighttp/server.go b/config/confighttp/server.go index e3bd23d5e3ca..57a7feebdf74 100644 --- a/config/confighttp/server.go +++ b/config/confighttp/server.go @@ -97,12 +97,6 @@ type ServerConfig struct { // KeepAlivesEnabled controls whether HTTP keep-alives are enabled. // By default, keep-alives are always enabled. Only very resource-constrained environments should disable them. KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"` - - // ReusePort enables the SO_REUSEPORT socket option on the listener. - // This allows multiple server instances to bind to the same address/port. - // This is useful for horizontal scaling and zero-downtime restarts. - // Note: This option is not supported on all operating systems. - ReusePort bool `mapstructure:"reuse_port,omitempty"` } // NewDefaultServerConfig returns ServerConfig type object with default values. @@ -127,23 +121,9 @@ type AuthConfig struct { _ struct{} } -func (sc *ServerConfig) Validate() error { - _, err := sc.getListenConfig() - if err != nil { - return err - } - - return nil -} - // ToListener creates a net.Listener. func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) { - cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations - if err != nil { - return nil, err - } - - listener, err := cfg.Listen(ctx, "tcp", sc.Endpoint) + listener, err := net.Listen("tcp", sc.Endpoint) if err != nil { return nil, err } diff --git a/config/confighttp/server_test.go b/config/confighttp/server_test.go index 03b31a5ba7e3..5db0af79a811 100644 --- a/config/confighttp/server_test.go +++ b/config/confighttp/server_test.go @@ -12,7 +12,6 @@ import ( "net/http" "net/http/httptest" "path/filepath" - "runtime" "strconv" "strings" "testing" @@ -1146,70 +1145,3 @@ func TestServerMiddlewaresFieldCompatibility(t *testing.T) { assert.Len(t, serverConfig.Middlewares, 1) assert.Equal(t, component.MustNewID("server_middleware"), serverConfig.Middlewares[0].ID) } - -func TestServerReusePort(t *testing.T) { - if runtime.GOOS == "windows" { - sc := &ServerConfig{ - Endpoint: "localhost:4318", - ReusePort: true, - } - - _, err := sc.ToListener(t.Context()) - require.Error(t, err, "ReusePort is not supported on Windows") - return - } - - tests := []struct { - name string - reusePort bool - expectedError bool - }{ - { - name: "ReusePort enabled", - reusePort: true, - expectedError: false, - }, - { - name: "ReusePort disabled", - reusePort: false, - expectedError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sc := &ServerConfig{ - Endpoint: "localhost:4318", - ReusePort: tt.reusePort, - } - - ln1, err := sc.ToListener(t.Context()) - require.NoError(t, err) - defer ln1.Close() - - ln2, err := sc.ToListener(t.Context()) - if tt.expectedError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - - if ln2 != nil { - ln2.Close() - } - }) - } -} - -func TestServerConfigValidate(t *testing.T) { - sc := &ServerConfig{ - Endpoint: "localhost:4318", - ReusePort: true, - } - - if runtime.GOOS == "windows" { - require.Error(t, sc.Validate()) - } else { - require.NoError(t, sc.Validate()) - } -} diff --git a/config/confignet/confignet.go b/config/confignet/confignet.go index 5cbb3ff114d5..6250877d7f3d 100644 --- a/config/confignet/confignet.go +++ b/config/confignet/confignet.go @@ -84,6 +84,13 @@ type AddrConfig struct { // DialerConfig contains options for connecting to an address. DialerConfig DialerConfig `mapstructure:"dialer,omitempty"` + + // ReusePort enables the SO_REUSEPORT socket option on the listener. + // This allows multiple server instances to bind to the same address/port. + // This is useful for horizontal scaling and zero-downtime restarts. + // Note: This option is not supported on all operating systems. + ReusePort bool `mapstructure:"reuse_port,omitempty"` + // prevent unkeyed literal initialization _ struct{} } @@ -103,7 +110,10 @@ func (na *AddrConfig) Dial(ctx context.Context) (net.Conn, error) { // Listen equivalent with net.ListenConfig's Listen for this address. func (na *AddrConfig) Listen(ctx context.Context) (net.Listener, error) { - lc := net.ListenConfig{} + lc, err := na.getListenConfig() + if err != nil { + return nil, err + } return lc.Listen(ctx, string(na.Transport), na.Endpoint) } diff --git a/config/confignet/confignet_test.go b/config/confignet/confignet_test.go index 84ea79373d85..9d69bec557fb 100644 --- a/config/confignet/confignet_test.go +++ b/config/confignet/confignet_test.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net" + "runtime" "testing" "time" @@ -159,3 +160,73 @@ func Test_TransportType_UnmarshalText(t *testing.T) { err = tt.UnmarshalText([]byte("invalid")) require.Error(t, err) } + +func TestServerReusePort(t *testing.T) { + if runtime.GOOS == "windows" { + sc := &AddrConfig{ + Endpoint: "localhost:4318", + Transport: TransportTypeTCP, + ReusePort: true, + } + + _, err := sc.Listen(t.Context()) + require.Error(t, err, "ReusePort is not supported on Windows") + return + } + + tests := []struct { + name string + reusePort bool + expectedError bool + }{ + { + name: "ReusePort enabled", + reusePort: true, + expectedError: false, + }, + { + name: "ReusePort disabled", + reusePort: false, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc := &AddrConfig{ + Endpoint: "localhost:4318", + Transport: TransportTypeTCP, + ReusePort: tt.reusePort, + } + + ln1, err := sc.Listen(t.Context()) + require.NoError(t, err) + defer ln1.Close() + + ln2, err := sc.Listen(t.Context()) + if tt.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + if ln2 != nil { + ln2.Close() + } + }) + } +} + +func TestServerConfigValidate(t *testing.T) { + sc := &AddrConfig{ + Endpoint: "localhost:4318", + Transport: TransportTypeTCP, + ReusePort: true, + } + + if runtime.GOOS == "windows" { + require.Error(t, sc.Validate()) + } else { + require.NoError(t, sc.Validate()) + } +} diff --git a/config/confignet/go.mod b/config/confignet/go.mod index 9fd5f557643f..b663ea329198 100644 --- a/config/confignet/go.mod +++ b/config/confignet/go.mod @@ -5,6 +5,7 @@ go 1.24.0 require ( github.com/stretchr/testify v1.11.1 go.uber.org/goleak v1.3.0 + golang.org/x/sys v0.39.0 ) require ( diff --git a/config/confignet/go.sum b/config/confignet/go.sum index f989d17ba1ed..02b8bd41ec50 100644 --- a/config/confignet/go.sum +++ b/config/confignet/go.sum @@ -18,6 +18,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/config/confighttp/listen_config_other.go b/config/confignet/listen_config_other.go similarity index 77% rename from config/confighttp/listen_config_other.go rename to config/confignet/listen_config_other.go index 9776ae1825d7..5185646d9e04 100644 --- a/config/confighttp/listen_config_other.go +++ b/config/confignet/listen_config_other.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 //go:build !windows -package confighttp // import "go.opentelemetry.io/collector/config/confighttp" +package confignet // import "go.opentelemetry.io/collector/config/confignet" import ( "net" @@ -11,7 +11,7 @@ import ( "golang.org/x/sys/unix" ) -func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { +func (sc *AddrConfig) getListenConfig() (net.ListenConfig, error) { cfg := net.ListenConfig{} if sc.ReusePort { cfg.Control = func(_, _ string, c syscall.RawConn) error { diff --git a/config/confighttp/listen_config_windows.go b/config/confignet/listen_config_windows.go similarity index 64% rename from config/confighttp/listen_config_windows.go rename to config/confignet/listen_config_windows.go index 2a0a62ce04b9..100ba463a5d3 100644 --- a/config/confighttp/listen_config_windows.go +++ b/config/confignet/listen_config_windows.go @@ -2,14 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 //go:build windows -package confighttp // import "go.opentelemetry.io/collector/config/confighttp" +package confignet // import "go.opentelemetry.io/collector/config/confignet" import ( "errors" "net" ) -func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) { +func (sc *AddrConfig) getListenConfig() (net.ListenConfig, error) { if sc.ReusePort { return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") } From 0a4b0fbea5180fea231d679cdf9a5e15084c2afa Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Mon, 5 Jan 2026 11:34:52 +0000 Subject: [PATCH 08/15] Update changelog entry Signed-off-by: sinkingpoint --- .chloggen/so-reuse-port.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.chloggen/so-reuse-port.yaml b/.chloggen/so-reuse-port.yaml index 4b66241a50ab..05f672216e9e 100644 --- a/.chloggen/so-reuse-port.yaml +++ b/.chloggen/so-reuse-port.yaml @@ -4,11 +4,11 @@ change_type: enhancement # The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver) -component: pkg/config/confighttp +component: pkg/config/confignet # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). # cspell:ignore REUSEPORT -note: Added ReusePort option to confighttp.ServerConfig to enable SO_REUSEPORT on supported platforms. +note: Added ReusePort option to confignet.AddrConfig to enable SO_REUSEPORT on supported platforms. # One or more tracking issues or pull requests related to the change issues: [14046] From 9e06eca4be95405c50307d9bcdbe955ab082b9f8 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Mon, 5 Jan 2026 11:51:39 +0000 Subject: [PATCH 09/15] CI fixes Signed-off-by: sinkingpoint --- config/confignet/confignet.go | 5 +++++ config/confignet/listen_config_other.go | 4 ++-- config/confignet/listen_config_windows.go | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config/confignet/confignet.go b/config/confignet/confignet.go index 6250877d7f3d..bd5dd9b226d4 100644 --- a/config/confignet/confignet.go +++ b/config/confignet/confignet.go @@ -118,6 +118,11 @@ func (na *AddrConfig) Listen(ctx context.Context) (net.Listener, error) { } func (na *AddrConfig) Validate() error { + _, err := na.getListenConfig() + if err != nil { + return err + } + switch na.Transport { case TransportTypeTCP, TransportTypeTCP4, diff --git a/config/confignet/listen_config_other.go b/config/confignet/listen_config_other.go index 5185646d9e04..b71ef52c2393 100644 --- a/config/confignet/listen_config_other.go +++ b/config/confignet/listen_config_other.go @@ -11,9 +11,9 @@ import ( "golang.org/x/sys/unix" ) -func (sc *AddrConfig) getListenConfig() (net.ListenConfig, error) { +func (na *AddrConfig) getListenConfig() (net.ListenConfig, error) { cfg := net.ListenConfig{} - if sc.ReusePort { + if na.ReusePort { cfg.Control = func(_, _ string, c syscall.RawConn) error { var controlErr error err := c.Control(func(fd uintptr) { diff --git a/config/confignet/listen_config_windows.go b/config/confignet/listen_config_windows.go index 100ba463a5d3..1a2d6cb7f714 100644 --- a/config/confignet/listen_config_windows.go +++ b/config/confignet/listen_config_windows.go @@ -9,8 +9,8 @@ import ( "net" ) -func (sc *AddrConfig) getListenConfig() (net.ListenConfig, error) { - if sc.ReusePort { +func (na *AddrConfig) getListenConfig() (net.ListenConfig, error) { + if na.ReusePort { return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") } From dc599afd367c59b767332e6b572b1ea8c4b45256 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Mon, 12 Jan 2026 14:37:01 +0000 Subject: [PATCH 10/15] Shuffle listen_config files As per PR comments, this moves things around so that listen_config_other becomes listen_config_unix, and listen_config_windows becomes listen_config_other, with appropriate build gating. Signed-off-by: sinkingpoint --- config/confignet/listen_config_other.go | 20 +++------------ config/confignet/listen_config_unix.go | 30 +++++++++++++++++++++++ config/confignet/listen_config_windows.go | 18 -------------- 3 files changed, 34 insertions(+), 34 deletions(-) create mode 100644 config/confignet/listen_config_unix.go delete mode 100644 config/confignet/listen_config_windows.go diff --git a/config/confignet/listen_config_other.go b/config/confignet/listen_config_other.go index b71ef52c2393..6f014061d6ad 100644 --- a/config/confignet/listen_config_other.go +++ b/config/confignet/listen_config_other.go @@ -1,30 +1,18 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -//go:build !windows +//go:build !(linux || darwin) package confignet // import "go.opentelemetry.io/collector/config/confignet" import ( + "errors" "net" - "syscall" - - "golang.org/x/sys/unix" ) func (na *AddrConfig) getListenConfig() (net.ListenConfig, error) { - cfg := net.ListenConfig{} if na.ReusePort { - cfg.Control = func(_, _ string, c syscall.RawConn) error { - var controlErr error - err := c.Control(func(fd uintptr) { - controlErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) - }) - if err != nil { - return err - } - return controlErr - } + return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") } - return cfg, nil + return net.ListenConfig{}, nil } diff --git a/config/confignet/listen_config_unix.go b/config/confignet/listen_config_unix.go new file mode 100644 index 000000000000..509687b5510f --- /dev/null +++ b/config/confignet/listen_config_unix.go @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 +//go:build linux || darwin + +package confignet // import "go.opentelemetry.io/collector/config/confignet" + +import ( + "net" + "syscall" + + "golang.org/x/sys/unix" +) + +func (na *AddrConfig) getListenConfig() (net.ListenConfig, error) { + cfg := net.ListenConfig{} + if na.ReusePort { + cfg.Control = func(_, _ string, c syscall.RawConn) error { + var controlErr error + err := c.Control(func(fd uintptr) { + controlErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + if err != nil { + return err + } + return controlErr + } + } + + return cfg, nil +} diff --git a/config/confignet/listen_config_windows.go b/config/confignet/listen_config_windows.go deleted file mode 100644 index 1a2d6cb7f714..000000000000 --- a/config/confignet/listen_config_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 -//go:build windows - -package confignet // import "go.opentelemetry.io/collector/config/confignet" - -import ( - "errors" - "net" -) - -func (na *AddrConfig) getListenConfig() (net.ListenConfig, error) { - if na.ReusePort { - return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform") - } - - return net.ListenConfig{}, nil -} From 1ac28b2e6b18db19b9c4fd9854dc1e31d97431c1 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Thu, 19 Feb 2026 09:39:00 +0000 Subject: [PATCH 11/15] Document reuse-port setting more Signed-off-by: sinkingpoint --- config/confignet/README.md | 1 + config/confignet/confignet.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/confignet/README.md b/config/confignet/README.md index 02bc3d1b2372..2188c35b0c9b 100644 --- a/config/confignet/README.md +++ b/config/confignet/README.md @@ -13,6 +13,7 @@ leverage network configuration to set connection and transport information. - `transport`: Known protocols are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket". +- `reuse_port`: If set to `true`, enables SO_REUSEPOST on the listener, allowing - `dialer`: Dialer configuration - `timeout`: Dialer timeout is the maximum amount of time a dial will wait for a connect to complete. The default is no timeout. diff --git a/config/confignet/confignet.go b/config/confignet/confignet.go index bd5dd9b226d4..77abb14cb905 100644 --- a/config/confignet/confignet.go +++ b/config/confignet/confignet.go @@ -88,7 +88,7 @@ type AddrConfig struct { // ReusePort enables the SO_REUSEPORT socket option on the listener. // This allows multiple server instances to bind to the same address/port. // This is useful for horizontal scaling and zero-downtime restarts. - // Note: This option is not supported on all operating systems. + // Note: This option is only supported on Linux and Darwin-based operating systems. ReusePort bool `mapstructure:"reuse_port,omitempty"` // prevent unkeyed literal initialization From f5ef1d5036ea55fd528c129f03e7eada656134cc Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Thu, 19 Feb 2026 09:41:13 +0000 Subject: [PATCH 12/15] Fix spell check on confignet readme Signed-off-by: sinkingpoint --- config/confignet/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/confignet/README.md b/config/confignet/README.md index 2188c35b0c9b..4538a3605130 100644 --- a/config/confignet/README.md +++ b/config/confignet/README.md @@ -13,7 +13,8 @@ leverage network configuration to set connection and transport information. - `transport`: Known protocols are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket". -- `reuse_port`: If set to `true`, enables SO_REUSEPOST on the listener, allowing + +- `reuse_port`: If set to `true`, enables the SO_REUSEPORT socket option on the listener, allowing multiple processes to listen on the same port - `dialer`: Dialer configuration - `timeout`: Dialer timeout is the maximum amount of time a dial will wait for a connect to complete. The default is no timeout. From 1e6b99415d012942814ae79d9b51bc94d0022f80 Mon Sep 17 00:00:00 2001 From: sinkingpoint Date: Thu, 19 Feb 2026 09:42:29 +0000 Subject: [PATCH 13/15] Update ReusePort tests to properly scope checks This changes the if runtime.GOOS == "windows" { to properly match the updated build tags Signed-off-by: sinkingpoint --- config/confignet/confignet_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/confignet/confignet_test.go b/config/confignet/confignet_test.go index 9d69bec557fb..04799137ea3a 100644 --- a/config/confignet/confignet_test.go +++ b/config/confignet/confignet_test.go @@ -162,7 +162,7 @@ func Test_TransportType_UnmarshalText(t *testing.T) { } func TestServerReusePort(t *testing.T) { - if runtime.GOOS == "windows" { + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { sc := &AddrConfig{ Endpoint: "localhost:4318", Transport: TransportTypeTCP, @@ -224,7 +224,7 @@ func TestServerConfigValidate(t *testing.T) { ReusePort: true, } - if runtime.GOOS == "windows" { + if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { require.Error(t, sc.Validate()) } else { require.NoError(t, sc.Validate()) From 27e7524d0c98e35e5ac4e0204f52ea575ea07f49 Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Thu, 2 Apr 2026 11:03:50 +0800 Subject: [PATCH 14/15] Add SO_REUSEPORT to cspell.json --- .chloggen/so-reuse-port.yaml | 1 - .github/workflows/utils/cspell.json | 1 + config/confignet/README.md | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.chloggen/so-reuse-port.yaml b/.chloggen/so-reuse-port.yaml index 05f672216e9e..595a213a8b12 100644 --- a/.chloggen/so-reuse-port.yaml +++ b/.chloggen/so-reuse-port.yaml @@ -7,7 +7,6 @@ change_type: enhancement component: pkg/config/confignet # A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). -# cspell:ignore REUSEPORT note: Added ReusePort option to confignet.AddrConfig to enable SO_REUSEPORT on supported platforms. # One or more tracking issues or pull requests related to the change diff --git a/.github/workflows/utils/cspell.json b/.github/workflows/utils/cspell.json index 8db53b22a41b..6af947904a7d 100644 --- a/.github/workflows/utils/cspell.json +++ b/.github/workflows/utils/cspell.json @@ -71,6 +71,7 @@ "RCPC", "Rahul", "SASL", + "SO_REUSEPORT", "Samplingdecision", "Sharma", "Statefulness", diff --git a/config/confignet/README.md b/config/confignet/README.md index 4538a3605130..16b74980d9a3 100644 --- a/config/confignet/README.md +++ b/config/confignet/README.md @@ -13,7 +13,6 @@ leverage network configuration to set connection and transport information. - `transport`: Known protocols are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp", "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only), "unix", "unixgram" and "unixpacket". - - `reuse_port`: If set to `true`, enables the SO_REUSEPORT socket option on the listener, allowing multiple processes to listen on the same port - `dialer`: Dialer configuration - `timeout`: Dialer timeout is the maximum amount of time a dial will wait for a connect to complete. The default is no timeout. From c70d0d881da63da9244c895866e781505c8d61e0 Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Thu, 2 Apr 2026 11:06:04 +0800 Subject: [PATCH 15/15] Fix error assertion --- config/confignet/confignet_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/confignet/confignet_test.go b/config/confignet/confignet_test.go index 04799137ea3a..6298c4f43fa3 100644 --- a/config/confignet/confignet_test.go +++ b/config/confignet/confignet_test.go @@ -170,7 +170,7 @@ func TestServerReusePort(t *testing.T) { } _, err := sc.Listen(t.Context()) - require.Error(t, err, "ReusePort is not supported on Windows") + require.EqualError(t, err, "ReusePort is not supported on this platform") return }