Skip to content
This repository has been archived by the owner on Mar 15, 2024. It is now read-only.

Commit

Permalink
Refactoring web HTTP
Browse files Browse the repository at this point in the history
  • Loading branch information
im-kulikov committed Apr 19, 2021
1 parent a3ebb85 commit 5f0ed8a
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 71 deletions.
126 changes: 66 additions & 60 deletions web/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import (
"fmt"
"net"
"net/http"
"time"

"go.uber.org/zap"

"github.com/im-kulikov/helium/internal"
"github.com/im-kulikov/helium/service"
)

type (
httpService struct {
skipErrors bool
name string
address string
network string
server *http.Server
shutdownTimeout time.Duration
logger *zap.Logger

skipErrors bool
name string
address string
network string
listener net.Listener
server *http.Server
}

// HTTPOption interface that allows
Expand All @@ -27,30 +30,21 @@ type (
)

const (
// ErrEmptyHTTPServer is raised when called New
// or httpService methods with empty http.Server.
// ErrEmptyHTTPServer is raised when called New or httpService methods with empty http.Server.
ErrEmptyHTTPServer = internal.Error("empty http server")

// ErrEmptyHTTPAddress is raised when passed empty address to NewHTTPService.
ErrEmptyHTTPAddress = internal.Error("empty http address")
)

// HTTPShutdownTimeout changes default shutdown timeout.
func HTTPShutdownTimeout(v time.Duration) HTTPOption {
return func(s *httpService) {
s.shutdownTimeout = v
}
}

// HTTPName allows set name for the http-service.
func HTTPName(name string) HTTPOption {
return func(s *httpService) {
s.name = name
}
}

// HTTPListenNetwork allows to change default (tcp)
// network for net.Listener.
// HTTPListenNetwork allows to change default (tcp) network for net.Listener.
func HTTPListenNetwork(network string) HTTPOption {
return func(s *httpService) {
s.network = network
Expand All @@ -65,90 +59,102 @@ func HTTPListenAddress(address string) HTTPOption {
}
}

// HTTPSkipErrors allows to skip any errors
// HTTPSkipErrors allows to skip any errors.
func HTTPSkipErrors() HTTPOption {
return func(s *httpService) {
s.skipErrors = true
}
}

// HTTPListener allows to set custom net.Listener.
func HTTPListener(lis net.Listener) HTTPOption {
return func(s *httpService) {
if lis == nil {
return
}

s.listener = lis
}
}

// HTTPWithLogger allows to set logger.
func HTTPWithLogger(l *zap.Logger) HTTPOption {
return func(s *httpService) {
if l == nil {
return
}

s.logger = l
}
}

// NewHTTPService creates Service from http.Server and HTTPOption's.
func NewHTTPService(serve *http.Server, opts ...HTTPOption) (service.Service, error) {
if serve == nil {
return nil, ErrEmptyHTTPServer
}

s := &httpService{
skipErrors: false,
server: serve,
network: "tcp",
address: serve.Addr,
shutdownTimeout: time.Second * 30,
logger: zap.NewNop(),

skipErrors: false,
server: serve,
network: "tcp",
}

for i := range opts {
opts[i](s)
}

if s.listener != nil {
return s, nil
}

if s.address == "" {
return nil, ErrEmptyHTTPAddress
}

var err error
if s.listener, err = net.Listen(s.network, s.address); err != nil {
return nil, s.catch(err)
}

return s, nil
}

// Name returns name of the service.
func (s *httpService) Name() string {
return fmt.Sprintf("http(%s) %s %s", s.name, s.network, s.address)
return fmt.Sprintf("http(%s) %s", s.name, s.listener.Addr())
}

// Start runs http.Server and returns error
// if something went wrong.
func (s *httpService) Start(ctx context.Context) error {
var (
err error
lis net.Listener
lic net.ListenConfig
)

func (s *httpService) Start(context.Context) error {
if s.server == nil {
return s.catch(ErrEmptyHTTPServer)
} else if lis, err = lic.Listen(ctx, s.network, s.address); err != nil {
return s.catch(err)
return ErrEmptyHTTPServer
}

go func() {
var err error

switch {
case s.server.TLSConfig == nil:
err = s.server.Serve(lis)
default:
// provide cert and key from TLSConfig
err = s.server.ServeTLS(lis, "", "")
}

// ignores known error
if err = s.catch(err); err != nil {
fmt.Printf("could not start http.Server: %v\n", err)
fatal(2)
}
}()

return nil
switch {
case s.server.TLSConfig == nil:
return s.catch(s.server.Serve(s.listener))
default:
// provide cert and key from TLSConfig
return s.catch(s.server.ServeTLS(s.listener, "", ""))
}
}

// Stop tries to stop http.Server and returns error
// if something went wrong.
func (s *httpService) Stop() error {
func (s *httpService) Stop(ctx context.Context) {
if s.server == nil {
return s.catch(ErrEmptyHTTPServer)
panic(ErrEmptyHTTPServer)
}

ctx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout)
defer cancel()

return s.catch(s.server.Shutdown(ctx))
if err := s.catch(s.server.Shutdown(ctx)); err != nil {
s.logger.Error("could not stop http.Server",
zap.String("name", s.name),
zap.Error(err))
}
}

func (s *httpService) catch(err error) error {
Expand Down
80 changes: 69 additions & 11 deletions web/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,35 @@ import (
"net"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"google.golang.org/grpc/test/bufconn"

"github.com/im-kulikov/helium/group"
)

func TestHTTPService(t *testing.T) {
t.Run("should set network", func(t *testing.T) {
t.Run("should set network and address", func(t *testing.T) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
require.NoError(t, lis.Close())

serve, err := NewHTTPService(
&http.Server{},
HTTPSkipErrors(),
HTTPListenAddress(":8080"),
HTTPListenNetwork("test"))
HTTPName(apiServer),
HTTPListener(nil),
HTTPWithLogger(nil),
HTTPListenAddress(lis.Addr().String()),
HTTPListenNetwork(lis.Addr().Network()))
require.NoError(t, err)

s, ok := serve.(*httpService)
require.True(t, ok)
require.Equal(t, "test", s.network)
require.Equal(t, lis.Addr().String(), s.address)
require.Equal(t, lis.Addr().Network(), s.network)
})

t.Run("should fail on empty address", func(t *testing.T) {
Expand All @@ -39,18 +52,52 @@ func TestHTTPService(t *testing.T) {

t.Run("should fail on Start and Stop", func(t *testing.T) {
require.EqualError(t, (&httpService{}).Start(context.Background()), ErrEmptyHTTPServer.Error())
require.EqualError(t, (&httpService{}).Stop(), ErrEmptyHTTPServer.Error())
require.Panics(t, func() {
(&httpService{}).Stop(context.Background())
}, ErrEmptyHTTPServer.Error())
})

t.Run("should fail on net.Listen", func(t *testing.T) {
require.EqualError(t, (&httpService{server: &http.Server{}}).Start(context.Background()), "listen: unknown network ")
srv, err := NewHTTPService(&http.Server{}, HTTPListenAddress("test:80"))
require.Nil(t, srv)
require.EqualError(t, err, "listen tcp: lookup test: no such host")
})

t.Run("should not fail for tls", func(t *testing.T) {
lis, err := net.Listen("tcp", "127.0.0.1:0")
t.Run("should fail for serve", func(t *testing.T) {
lis := bufconn.Listen(listenSize)
s := &http.Server{}

serve, err := NewHTTPService(s,
HTTPListener(lis),
HTTPWithLogger(zaptest.NewLogger(t)))
require.NoError(t, err)
require.NoError(t, lis.Close())
require.NotEmpty(t, serve.Name())

ctx, cancel := context.WithCancel(context.Background())
require.NoError(t, group.New().
Add(serve.Start, func(context.Context) { serve.Stop(ctx) }).
Add(func(ctx context.Context) error {
cancel()

con, errConn := lis.Dial()
if errConn != nil {
return errConn
}

go func() {
// emulate long query:
time.Sleep(time.Second)

_ = con.Close()
}()

return nil
}, func(context.Context) {}).
Run(ctx))
})

t.Run("should not fail for tls", func(t *testing.T) {
lis := bufconn.Listen(listenSize)
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
Expand All @@ -59,9 +106,20 @@ func TestHTTPService(t *testing.T) {
},
}

serve, err := NewHTTPService(s, HTTPListenAddress(lis.Addr().String()))
serve, err := NewHTTPService(s,
HTTPListener(lis),
HTTPWithLogger(zaptest.NewLogger(t)))
require.NoError(t, err)
require.NotEmpty(t, serve.Name())
require.NoError(t, serve.Start(context.Background()))

ctx, cancel := context.WithCancel(context.Background())
require.NoError(t, group.New().
Add(serve.Start, serve.Stop).
Add(func(context.Context) error {
cancel()

return nil
}, func(context.Context) {}).
Run(ctx))
})
}

0 comments on commit 5f0ed8a

Please sign in to comment.