Skip to content

Commit bac556b

Browse files
committed
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 <[email protected]>
1 parent 71418b6 commit bac556b

File tree

6 files changed

+134
-2
lines changed

6 files changed

+134
-2
lines changed

.chloggen/so-reuse-port.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: pkg/config/confighttp
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Added ReusePort option to confighttp.ServerConfig to enable SO_REUSEPORT on supported platforms.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [14046]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: []

config/confighttp/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ require (
6666
go.uber.org/multierr v1.11.0 // indirect
6767
go.yaml.in/yaml/v3 v3.0.4 // indirect
6868
golang.org/x/crypto v0.41.0 // indirect
69-
golang.org/x/sys v0.35.0 // indirect
69+
golang.org/x/sys v0.35.0
7070
golang.org/x/text v0.28.0 // indirect
7171
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
7272
google.golang.org/grpc v1.76.0 // indirect
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
//go:build !windows
4+
5+
package confighttp // import "go.opentelemetry.io/collector/config/confighttp"
6+
7+
import (
8+
"net"
9+
"syscall"
10+
11+
"golang.org/x/sys/unix"
12+
)
13+
14+
func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) {
15+
cfg := net.ListenConfig{}
16+
if sc.ReusePort {
17+
cfg.Control = func(_, _ string, c syscall.RawConn) error {
18+
var controlErr error
19+
err := c.Control(func(fd uintptr) {
20+
controlErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
21+
})
22+
if err != nil {
23+
return err
24+
}
25+
return controlErr
26+
}
27+
}
28+
29+
return cfg, nil
30+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
//go:build windows
4+
5+
package confighttp // import "go.opentelemetry.io/collector/config/confighttp"
6+
7+
import (
8+
"errors"
9+
"net"
10+
)
11+
12+
func (sc *ServerConfig) getListenConfig() (net.ListenConfig, error) {
13+
if sc.ReusePort {
14+
return net.ListenConfig{}, errors.New("ReusePort is not supported on this platform")
15+
}
16+
17+
return net.ListenConfig{}, nil
18+
}

config/confighttp/server.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ type ServerConfig struct {
9696
// KeepAlivesEnabled controls whether HTTP keep-alives are enabled.
9797
// By default, keep-alives are always enabled. Only very resource-constrained environments should disable them.
9898
KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"`
99+
100+
// ReusePort enables the SO_REUSEPORT socket option on the listener.
101+
// This allows multiple server instances to bind to the same address/port.
102+
// This is useful for horizontal scaling and zero-downtime restarts.
103+
// Note: This option is not supported on all operating systems.
104+
ReusePort bool `mapstructure:"reuse_port,omitempty"`
99105
}
100106

101107
// NewDefaultServerConfig returns ServerConfig type object with default values.
@@ -123,7 +129,12 @@ type AuthConfig struct {
123129

124130
// ToListener creates a net.Listener.
125131
func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) {
126-
listener, err := net.Listen("tcp", sc.Endpoint)
132+
cfg, err := sc.getListenConfig() // See listen_config_*.go for platform-specific implementations
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
listener, err := cfg.Listen(ctx, "tcp", sc.Endpoint)
127138
if err != nil {
128139
return nil, err
129140
}

config/confighttp/server_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/http"
1313
"net/http/httptest"
1414
"path/filepath"
15+
"runtime"
1516
"strconv"
1617
"strings"
1718
"testing"
@@ -1169,3 +1170,50 @@ func TestServerMiddlewaresFieldCompatibility(t *testing.T) {
11691170
assert.Len(t, serverConfig.Middlewares, 1)
11701171
assert.Equal(t, component.MustNewID("server_middleware"), serverConfig.Middlewares[0].ID)
11711172
}
1173+
1174+
func TestServerReusePort(t *testing.T) {
1175+
if runtime.GOOS == "windows" {
1176+
t.Skip("skipping test: SO_REUSEPORT is not supported on windows")
1177+
}
1178+
1179+
tests := []struct {
1180+
name string
1181+
reusePort bool
1182+
expectedError bool
1183+
}{
1184+
{
1185+
name: "ReusePort enabled",
1186+
reusePort: true,
1187+
expectedError: false,
1188+
},
1189+
{
1190+
name: "ReusePort disabled",
1191+
reusePort: false,
1192+
expectedError: true,
1193+
},
1194+
}
1195+
1196+
for _, tt := range tests {
1197+
t.Run(tt.name, func(t *testing.T) {
1198+
sc := &ServerConfig{
1199+
Endpoint: "localhost:4318",
1200+
ReusePort: tt.reusePort,
1201+
}
1202+
1203+
ln1, err := sc.ToListener(t.Context())
1204+
require.NoError(t, err)
1205+
defer ln1.Close()
1206+
1207+
ln2, err := sc.ToListener(t.Context())
1208+
if tt.expectedError {
1209+
require.Error(t, err)
1210+
} else {
1211+
require.NoError(t, err)
1212+
}
1213+
1214+
if ln2 != nil {
1215+
ln2.Close()
1216+
}
1217+
})
1218+
}
1219+
}

0 commit comments

Comments
 (0)