diff --git a/CHANGELOG.md b/CHANGELOG.md index 40bf6efed..d26c00e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -203,6 +203,7 @@ * `gate_duration_seconds` * `kv_request_duration_seconds` * `operation_duration_seconds` +* [ENHANCEMENT] Implement UNIX socket support for gRPC and HTTP listeners. #511 * [BUGFIX] spanlogger: Support multiple tenant IDs. #59 * [BUGFIX] Memberlist: fixed corrupted packets when sending compound messages with more than 255 messages or messages bigger than 64KB. #85 * [BUGFIX] Ring: `ring_member_ownership_percent` and `ring_tokens_owned` metrics are not updated on scale down. #109 diff --git a/server/server.go b/server/server.go index c39e3873c..71843fe96 100644 --- a/server/server.go +++ b/server/server.go @@ -48,6 +48,8 @@ const ( DefaultNetwork = "tcp" // NetworkTCPV4 for IPV4 only NetworkTCPV4 = "tcp4" + // NetworkUnix for UNIX sockets + NetworkUnix = "unix" ) // SignalHandler used by Server. @@ -263,8 +265,16 @@ func newServer(cfg Config, metrics *Metrics) (*Server, error) { if network == "" { network = DefaultNetwork } + + // Generate a listen address depending on the configured network. + httpListenAddr := net.JoinHostPort(cfg.HTTPListenAddress, strconv.Itoa(cfg.HTTPListenPort)) + if network == "unix" { + // If we're using unix sockets instead of a TCP socket, don't set a port. + httpListenAddr = cfg.HTTPListenAddress + } + // Setup listeners first, so we can fail early if the port is in use. - httpListener, err := net.Listen(network, net.JoinHostPort(cfg.HTTPListenAddress, strconv.Itoa(cfg.HTTPListenPort))) + httpListener, err := net.Listen(network, httpListenAddr) if err != nil { return nil, err } @@ -282,7 +292,15 @@ func newServer(cfg Config, metrics *Metrics) (*Server, error) { if network == "" { network = DefaultNetwork } - grpcListener, err := net.Listen(network, net.JoinHostPort(cfg.GRPCListenAddress, strconv.Itoa(cfg.GRPCListenPort))) + + // Generate a listen address depending on the configured network. + grpcListenAddr := net.JoinHostPort(cfg.GRPCListenAddress, strconv.Itoa(cfg.GRPCListenPort)) + if network == "unix" { + // If we're using unix sockets instead of a TCP socket, don't set a port. + grpcListenAddr = cfg.GRPCListenAddress + } + + grpcListener, err := net.Listen(network, grpcListenAddr) if err != nil { return nil, err } diff --git a/server/server_test.go b/server/server_test.go index b15f4da26..74b1d01cf 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -128,6 +128,53 @@ func TestTCPv4Network(t *testing.T) { }) } +func TestUnixNetwork(t *testing.T) { + testSockDir, err := os.MkdirTemp("", "sock") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.RemoveAll(testSockDir)) + }) + + var cfg Config + setAutoAssignedPorts(NetworkUnix, &cfg) + cfg.HTTPListenAddress = filepath.Join(testSockDir, "http.sock") + cfg.GRPCListenAddress = filepath.Join(testSockDir, "grpc.sock") + + t.Run("unix_http", func(t *testing.T) { + var level log.Level + require.NoError(t, level.Set("info")) + cfg.LogLevel = level + cfg.MetricsNamespace = "testing_http_unix" + srv, err := New(cfg) + require.NoError(t, err) + + errChan := make(chan error, 1) + go func() { + errChan <- srv.Run() + }() + + require.NoError(t, srv.httpListener.Close()) + require.NotNil(t, <-errChan) + + // So that address is freed for further tests. + srv.GRPC.Stop() + }) + + t.Run("unix_http", func(t *testing.T) { + cfg.MetricsNamespace = "testing_grpc_unix" + srv, err := New(cfg) + require.NoError(t, err) + + errChan := make(chan error, 1) + go func() { + errChan <- srv.Run() + }() + + require.NoError(t, srv.grpcListener.Close()) + require.NotNil(t, <-errChan) + }) +} + // Ensure that http and grpc servers work with no overrides to config // (except http port because an ordinary user can't bind to default port 80) func TestDefaultAddresses(t *testing.T) {