mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 01:57:02 +00:00
Dependency injection improvements (#288)
Refactor server life-cycle into it's own file, make service startup and monitoring more consistent and testable. * Extract services creation in preparation for DI * pop3: rename New to NewServer * lifecycle: Add fatal error Notify() * web: Introduce Server struct w/ Notify() * Extract Start in lifecycle * Add Start() to Hub * RetentionScanner startup consistent with other svcs * Remove global shutdown channel * Implement a readiness notification system
This commit is contained in:
105
pkg/server/lifecycle.go
Normal file
105
pkg/server/lifecycle.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/inbucket/inbucket/pkg/config"
|
||||
"github.com/inbucket/inbucket/pkg/message"
|
||||
"github.com/inbucket/inbucket/pkg/msghub"
|
||||
"github.com/inbucket/inbucket/pkg/policy"
|
||||
"github.com/inbucket/inbucket/pkg/rest"
|
||||
"github.com/inbucket/inbucket/pkg/server/pop3"
|
||||
"github.com/inbucket/inbucket/pkg/server/smtp"
|
||||
"github.com/inbucket/inbucket/pkg/server/web"
|
||||
"github.com/inbucket/inbucket/pkg/storage"
|
||||
"github.com/inbucket/inbucket/pkg/stringutil"
|
||||
"github.com/inbucket/inbucket/pkg/webui"
|
||||
)
|
||||
|
||||
// Services holds the configured services.
|
||||
type Services struct {
|
||||
MsgHub *msghub.Hub
|
||||
POP3Server *pop3.Server
|
||||
RetentionScanner *storage.RetentionScanner
|
||||
SMTPServer *smtp.Server
|
||||
WebServer *web.Server
|
||||
notify chan error // Combined notification for failed services.
|
||||
ready *sync.WaitGroup // Tracks services that have not reported ready.
|
||||
}
|
||||
|
||||
// FullAssembly wires up a complete Inbucket environment.
|
||||
func FullAssembly(conf *config.Root) (*Services, error) {
|
||||
// Configure storage.
|
||||
store, err := storage.FromConfig(conf.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrPolicy := &policy.Addressing{Config: conf}
|
||||
msgHub := msghub.New(conf.Web.MonitorHistory)
|
||||
mmanager := &message.StoreManager{AddrPolicy: addrPolicy, Store: store, Hub: msgHub}
|
||||
|
||||
// Start Retention scanner.
|
||||
retentionScanner := storage.NewRetentionScanner(conf.Storage, store)
|
||||
|
||||
// Configure routes and build HTTP server.
|
||||
prefix := stringutil.MakePathPrefixer(conf.Web.BasePath)
|
||||
webui.SetupRoutes(web.Router.PathPrefix(prefix("/serve/")).Subrouter())
|
||||
rest.SetupRoutes(web.Router.PathPrefix(prefix("/api/")).Subrouter())
|
||||
webServer := web.NewServer(conf, mmanager, msgHub)
|
||||
|
||||
pop3Server := pop3.NewServer(conf.POP3, store)
|
||||
smtpServer := smtp.NewServer(conf.SMTP, mmanager, addrPolicy)
|
||||
|
||||
return &Services{
|
||||
MsgHub: msgHub,
|
||||
RetentionScanner: retentionScanner,
|
||||
POP3Server: pop3Server,
|
||||
SMTPServer: smtpServer,
|
||||
WebServer: webServer,
|
||||
ready: &sync.WaitGroup{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Start all services, returns immediately. Callers may use Notify to detect failed services.
|
||||
func (s *Services) Start(ctx context.Context, readyFunc func()) {
|
||||
go s.MsgHub.Start(ctx)
|
||||
go s.WebServer.Start(ctx, s.makeReadyFunc())
|
||||
go s.SMTPServer.Start(ctx, s.makeReadyFunc())
|
||||
go s.POP3Server.Start(ctx, s.makeReadyFunc())
|
||||
go s.RetentionScanner.Start(ctx)
|
||||
|
||||
// Notify when all services report ready.
|
||||
go func() {
|
||||
s.ready.Wait()
|
||||
readyFunc()
|
||||
}()
|
||||
}
|
||||
|
||||
// Notify merges the error notification channels of all fallible services, allowing the process to
|
||||
// be shutdown if needed.
|
||||
func (s *Services) Notify() <-chan error {
|
||||
c := make(chan error, 1)
|
||||
go func() {
|
||||
// TODO: What level to log failure.
|
||||
select {
|
||||
case err := <-s.POP3Server.Notify():
|
||||
c <- err
|
||||
case err := <-s.SMTPServer.Notify():
|
||||
c <- err
|
||||
case err := <-s.WebServer.Notify():
|
||||
c <- err
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *Services) makeReadyFunc() func() {
|
||||
s.ready.Add(1)
|
||||
var once sync.Once
|
||||
return func() {
|
||||
once.Do(s.ready.Done)
|
||||
}
|
||||
}
|
||||
@@ -13,47 +13,53 @@ import (
|
||||
|
||||
// Server defines an instance of the POP3 server.
|
||||
type Server struct {
|
||||
config config.POP3 // POP3 configuration.
|
||||
store storage.Store // Mail store.
|
||||
listener net.Listener // TCP listener.
|
||||
globalShutdown chan bool // Inbucket shutdown signal.
|
||||
wg *sync.WaitGroup // Waitgroup tracking sessions.
|
||||
config config.POP3 // POP3 configuration.
|
||||
store storage.Store // Mail store.
|
||||
listener net.Listener // TCP listener.
|
||||
wg *sync.WaitGroup // Waitgroup tracking sessions.
|
||||
notify chan error // Notify on fatal error.
|
||||
}
|
||||
|
||||
// New creates a new Server struct.
|
||||
func New(pop3Config config.POP3, shutdownChan chan bool, store storage.Store) *Server {
|
||||
// NewServer creates a new, unstarted, POP3 server.
|
||||
func NewServer(pop3Config config.POP3, store storage.Store) *Server {
|
||||
return &Server{
|
||||
config: pop3Config,
|
||||
store: store,
|
||||
globalShutdown: shutdownChan,
|
||||
wg: new(sync.WaitGroup),
|
||||
config: pop3Config,
|
||||
store: store,
|
||||
wg: new(sync.WaitGroup),
|
||||
notify: make(chan error, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server and listen for connections
|
||||
func (s *Server) Start(ctx context.Context) {
|
||||
func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
||||
slog := log.With().Str("module", "pop3").Str("phase", "startup").Logger()
|
||||
addr, err := net.ResolveTCPAddr("tcp4", s.config.Addr)
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to build tcp4 address")
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
slog.Info().Str("addr", addr.String()).Msg("POP3 listening on tcp4")
|
||||
s.listener, err = net.ListenTCP("tcp4", addr)
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to start tcp4 listener")
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
// Listener go routine.
|
||||
|
||||
// Start listener go routine.
|
||||
go s.serve(ctx)
|
||||
readyFunc()
|
||||
|
||||
// Wait for shutdown.
|
||||
select {
|
||||
case _ = <-ctx.Done():
|
||||
}
|
||||
slog = log.With().Str("module", "pop3").Str("phase", "shutdown").Logger()
|
||||
slog.Debug().Msg("POP3 shutdown requested, connections will be drained")
|
||||
|
||||
// Closing the listener will cause the serve() go routine to exit.
|
||||
if err := s.listener.Close(); err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to close POP3 listener")
|
||||
@@ -88,7 +94,8 @@ func (s *Server) serve(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
// Something went wrong.
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -100,18 +107,14 @@ func (s *Server) serve(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) emergencyShutdown() {
|
||||
// Shutdown Inbucket
|
||||
select {
|
||||
case _ = <-s.globalShutdown:
|
||||
default:
|
||||
close(s.globalShutdown)
|
||||
}
|
||||
}
|
||||
|
||||
// Drain causes the caller to block until all active POP3 sessions have finished
|
||||
func (s *Server) Drain() {
|
||||
// Wait for sessions to close
|
||||
s.wg.Wait()
|
||||
log.Debug().Str("module", "pop3").Str("phase", "shutdown").Msg("POP3 connections have drained")
|
||||
}
|
||||
|
||||
// Notify allows the running POP3 server to be monitored for a fatal error.
|
||||
func (s *Server) Notify() <-chan error {
|
||||
return s.notify
|
||||
}
|
||||
|
||||
@@ -484,13 +484,11 @@ func setupSMTPServer(ds storage.Store) (s *Server, buf *bytes.Buffer, teardown f
|
||||
buf = new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
// Create a server, don't start it.
|
||||
shutdownChan := make(chan bool)
|
||||
teardown = func() {
|
||||
close(shutdownChan)
|
||||
}
|
||||
// TODO Remove teardown.
|
||||
teardown = func() {}
|
||||
addrPolicy := &policy.Addressing{Config: cfg}
|
||||
manager := &message.StoreManager{Store: ds}
|
||||
s = NewServer(cfg.SMTP, shutdownChan, manager, addrPolicy)
|
||||
s = NewServer(cfg.SMTP, manager, addrPolicy)
|
||||
return s, buf, teardown
|
||||
}
|
||||
|
||||
|
||||
@@ -58,19 +58,18 @@ func init() {
|
||||
|
||||
// Server holds the configuration and state of our SMTP server.
|
||||
type Server struct {
|
||||
config config.SMTP // SMTP configuration.
|
||||
addrPolicy *policy.Addressing // Address policy.
|
||||
globalShutdown chan bool // Shuts down Inbucket.
|
||||
manager message.Manager // Used to deliver messages.
|
||||
listener net.Listener // Incoming network connections.
|
||||
wg *sync.WaitGroup // Waitgroup tracks individual sessions.
|
||||
tlsConfig *tls.Config
|
||||
config config.SMTP // SMTP configuration.
|
||||
addrPolicy *policy.Addressing // Address policy.
|
||||
manager message.Manager // Used to deliver messages.
|
||||
listener net.Listener // Incoming network connections.
|
||||
wg *sync.WaitGroup // Waitgroup tracks individual sessions.
|
||||
tlsConfig *tls.Config // TLS encryption configuration.
|
||||
notify chan error // Notify on fatal error.
|
||||
}
|
||||
|
||||
// NewServer creates a new Server instance with the specificed config.
|
||||
// NewServer creates a new, unstarted, SMTP server instance with the specificed config.
|
||||
func NewServer(
|
||||
smtpConfig config.SMTP,
|
||||
globalShutdown chan bool,
|
||||
manager message.Manager,
|
||||
apolicy *policy.Addressing,
|
||||
) *Server {
|
||||
@@ -90,37 +89,43 @@ func NewServer(
|
||||
}
|
||||
|
||||
return &Server{
|
||||
config: smtpConfig,
|
||||
globalShutdown: globalShutdown,
|
||||
manager: manager,
|
||||
addrPolicy: apolicy,
|
||||
wg: new(sync.WaitGroup),
|
||||
tlsConfig: tlsConfig,
|
||||
config: smtpConfig,
|
||||
manager: manager,
|
||||
addrPolicy: apolicy,
|
||||
wg: new(sync.WaitGroup),
|
||||
tlsConfig: tlsConfig,
|
||||
notify: make(chan error, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the listener and handle incoming connections.
|
||||
func (s *Server) Start(ctx context.Context) {
|
||||
func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
||||
slog := log.With().Str("module", "smtp").Str("phase", "startup").Logger()
|
||||
addr, err := net.ResolveTCPAddr("tcp4", s.config.Addr)
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to build tcp4 address")
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
slog.Info().Str("addr", addr.String()).Msg("SMTP listening on tcp4")
|
||||
s.listener, err = net.ListenTCP("tcp4", addr)
|
||||
if err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to start tcp4 listener")
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
// Listener go routine.
|
||||
|
||||
// Start listener go routine.
|
||||
go s.serve(ctx)
|
||||
readyFunc()
|
||||
|
||||
// Wait for shutdown.
|
||||
<-ctx.Done()
|
||||
slog = log.With().Str("module", "smtp").Str("phase", "shutdown").Logger()
|
||||
slog.Debug().Msg("SMTP shutdown requested, connections will be drained")
|
||||
|
||||
// Closing the listener will cause the serve() go routine to exit.
|
||||
if err := s.listener.Close(); err != nil {
|
||||
slog.Error().Err(err).Msg("Failed to close SMTP listener")
|
||||
@@ -156,7 +161,8 @@ func (s *Server) serve(ctx context.Context) {
|
||||
return
|
||||
default:
|
||||
// Something went wrong.
|
||||
s.emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -169,18 +175,14 @@ func (s *Server) serve(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) emergencyShutdown() {
|
||||
// Shutdown Inbucket.
|
||||
select {
|
||||
case <-s.globalShutdown:
|
||||
default:
|
||||
close(s.globalShutdown)
|
||||
}
|
||||
}
|
||||
|
||||
// Drain causes the caller to block until all active SMTP sessions have finished
|
||||
func (s *Server) Drain() {
|
||||
// Wait for sessions to close.
|
||||
s.wg.Wait()
|
||||
log.Debug().Str("module", "smtp").Str("phase", "shutdown").Msg("SMTP connections have drained")
|
||||
}
|
||||
|
||||
// Notify allows the running SMTP server to be monitored for a fatal error.
|
||||
func (s *Server) Notify() <-chan error {
|
||||
return s.notify
|
||||
}
|
||||
|
||||
@@ -31,10 +31,9 @@ var (
|
||||
// incoming requests to the correct handler function
|
||||
Router = mux.NewRouter()
|
||||
|
||||
rootConfig *config.Root
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
globalShutdown chan bool
|
||||
rootConfig *config.Root
|
||||
server *http.Server
|
||||
listener net.Listener
|
||||
|
||||
// ExpWebSocketConnectsCurrent tracks the number of open WebSockets
|
||||
ExpWebSocketConnectsCurrent = new(expvar.Int)
|
||||
@@ -45,15 +44,19 @@ func init() {
|
||||
m.Set("WebSocketConnectsCurrent", ExpWebSocketConnectsCurrent)
|
||||
}
|
||||
|
||||
// Initialize sets up things for unit tests or the Start() method.
|
||||
func Initialize(
|
||||
// Server defines an instance of the Web server.
|
||||
type Server struct {
|
||||
// TODO Migrate global vars here.
|
||||
notify chan error // Notify on fatal error.
|
||||
}
|
||||
|
||||
// NewServer sets up things for unit tests or the Start() method.
|
||||
func NewServer(
|
||||
conf *config.Root,
|
||||
shutdownChan chan bool,
|
||||
mm message.Manager,
|
||||
mh *msghub.Hub) {
|
||||
mh *msghub.Hub) *Server {
|
||||
|
||||
rootConfig = conf
|
||||
globalShutdown = shutdownChan
|
||||
|
||||
// NewContext() will use this DataStore for the web handlers.
|
||||
msgHub = mh
|
||||
@@ -118,10 +121,16 @@ func Initialize(
|
||||
http.StatusNotFound, "No route matches URI path")
|
||||
Router.MethodNotAllowedHandler = noMatchHandler(
|
||||
http.StatusMethodNotAllowed, "Method not allowed for URI path")
|
||||
|
||||
s := &Server{
|
||||
notify: make(chan error, 1),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start begins listening for HTTP requests
|
||||
func Start(ctx context.Context) {
|
||||
func (s *Server) Start(ctx context.Context, readyFunc func()) {
|
||||
server = &http.Server{
|
||||
Addr: rootConfig.Web.Addr,
|
||||
Handler: requestLoggingWrapper(Router),
|
||||
@@ -137,12 +146,14 @@ func Start(ctx context.Context) {
|
||||
if err != nil {
|
||||
log.Error().Str("module", "web").Str("phase", "startup").Err(err).
|
||||
Msg("HTTP failed to start TCP4 listener")
|
||||
emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
|
||||
// Listener go routine
|
||||
go serve(ctx)
|
||||
// Start listener go routine
|
||||
go s.serve(ctx)
|
||||
readyFunc()
|
||||
|
||||
// Wait for shutdown
|
||||
select {
|
||||
@@ -176,7 +187,7 @@ func appConfigCookie(webConfig config.Web) *http.Cookie {
|
||||
}
|
||||
|
||||
// serve begins serving HTTP requests
|
||||
func serve(ctx context.Context) {
|
||||
func (s *Server) serve(ctx context.Context) {
|
||||
// server.Serve blocks until we close the listener
|
||||
err := server.Serve(listener)
|
||||
|
||||
@@ -186,16 +197,13 @@ func serve(ctx context.Context) {
|
||||
default:
|
||||
log.Error().Str("module", "web").Str("phase", "startup").Err(err).
|
||||
Msg("HTTP server failed")
|
||||
emergencyShutdown()
|
||||
s.notify <- err
|
||||
close(s.notify)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func emergencyShutdown() {
|
||||
// Shutdown Inbucket
|
||||
select {
|
||||
case _ = <-globalShutdown:
|
||||
default:
|
||||
close(globalShutdown)
|
||||
}
|
||||
// Notify allows the running Web server to be monitored for a fatal error.
|
||||
func (s *Server) Notify() <-chan error {
|
||||
return s.notify
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user