Files
golib/hws/server.go
2026-02-03 18:43:31 +11:00

198 lines
4.3 KiB
Go

package hws
import (
"context"
"fmt"
"net/http"
"sync"
"time"
"git.haelnorr.com/h/golib/notify"
"k8s.io/apimachinery/pkg/util/validation"
"github.com/pkg/errors"
)
type Server struct {
GZIP bool
server *http.Server
logger *logger
routes bool
middleware bool
errorPage ErrorPageFunc
ready chan struct{}
notifier *Notifier
shutdowndelay time.Duration
}
// Ready returns a channel that is closed when the server is started
func (s *Server) Ready() <-chan struct{} {
return s.ready
}
// IsReady checks if the server is running
func (s *Server) IsReady() bool {
select {
case <-s.ready:
return true
default:
return false
}
}
// Addr returns the server's network address
func (s *Server) Addr() string {
return s.server.Addr
}
// Handler returns the server's HTTP handler for testing purposes
func (s *Server) Handler() http.Handler {
return s.server.Handler
}
// NewServer returns a new hws.Server with the specified configuration.
func NewServer(config *Config) (*Server, error) {
if config == nil {
return nil, errors.New("Config cannot be nil")
}
// Apply defaults for undefined fields
if config.Host == "" {
config.Host = "127.0.0.1"
}
if config.Port == 0 {
config.Port = 3000
}
if config.ReadHeaderTimeout == 0 {
config.ReadHeaderTimeout = 2 * time.Second
}
if config.WriteTimeout == 0 {
config.WriteTimeout = 10 * time.Second
}
if config.IdleTimeout == 0 {
config.IdleTimeout = 120 * time.Second
}
valid := isValidHostname(config.Host)
if !valid {
return nil, fmt.Errorf("hostname '%s' is not valid", config.Host)
}
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%v", config.Host, config.Port),
ReadHeaderTimeout: config.ReadHeaderTimeout,
WriteTimeout: config.WriteTimeout,
IdleTimeout: config.IdleTimeout,
}
server := &Server{
server: httpServer,
routes: false,
GZIP: config.GZIP,
ready: make(chan struct{}),
shutdowndelay: config.ShutdownDelay,
}
return server, nil
}
func (s *Server) Start(ctx context.Context) error {
if ctx == nil {
return errors.New("Context cannot be nil")
}
if !s.routes {
return errors.New("Server.AddRoutes must be run before starting the server")
}
if !s.middleware {
err := s.AddMiddleware()
if err != nil {
return errors.Wrap(err, "server.AddMiddleware")
}
}
s.startNotifier()
go func() {
if s.logger == nil {
fmt.Printf("Listening for requests on %s", s.server.Addr)
} else {
s.logger.logger.Info().Str("address", s.server.Addr).Msg("Listening for requests")
}
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if s.logger == nil {
fmt.Printf("Server encountered a fatal error: %s", err.Error())
} else {
s.LogError(HWSError{Error: err, Message: "Server encountered a fatal error"})
}
}
}()
s.waitUntilReady(ctx)
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
if s.logger != nil {
s.logger.logger.Debug().Dur("shutdown_delay", s.shutdowndelay).Msg("HWS Server shutting down")
}
s.NotifyAll(notify.Notification{
Title: "Shutting down",
Message: fmt.Sprintf("Server is shutting down in %v", s.shutdowndelay),
Level: LevelShutdown,
})
<-time.NewTimer(s.shutdowndelay).C
if !s.IsReady() {
return errors.New("Server isn't running")
}
if ctx == nil {
return errors.New("Context cannot be nil")
}
err := s.server.Shutdown(ctx)
if err != nil {
return errors.Wrap(err, "Failed to shutdown the server gracefully")
}
s.closeNotifier()
s.ready = make(chan struct{})
return nil
}
func isValidHostname(host string) bool {
// Validate as IP or hostname
if errs := validation.IsDNS1123Subdomain(host); len(errs) == 0 {
return true
}
// Check IPv4 / IPv6
if errs := validation.IsValidIP(nil, host); len(errs) == 0 {
return true
}
return false
}
func (s *Server) waitUntilReady(ctx context.Context) error {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
closeOnce := sync.Once{}
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
resp, err := http.Get("http://" + s.server.Addr + "/healthz")
if err != nil {
continue // not accepting yet
}
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
closeOnce.Do(func() { close(s.ready) })
return nil
}
}
}
}