7.3 KiB
7.3 KiB
Unified Server Scaffold Template
Generate the core unified server infrastructure in internal/common/server/. This replaces the standalone RunHTTPServer / RunGRPCServer functions with a composable server.New(...).Run(ctx) pattern that supports multiple transports with explicit shutdown ordering.
Created once per project. Individual transports (WithWatermillRouter) can be added later.
Placeholders
{{module_common}}— Go module path tointernal/common(e.g.,github.com/example/myproject/internal/common)
File 1: internal/common/server/server.go
package server
import (
"context"
"os/signal"
"sort"
"sync"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
type Server struct {
components map[string]component
startOrder []string
shutdownSteps []ShutdownStep
}
type component struct {
name string
start func(ctx context.Context) error
stop func(ctx context.Context) error
}
type Option func(*Server)
func New(opts ...Option) *Server {
s := &Server{
components: make(map[string]component),
}
for _, opt := range opts {
opt(s)
}
return s
}
func (s *Server) addComponent(name string, c component) {
if _, exists := s.components[name]; exists {
panic("duplicate component name: " + name)
}
s.components[name] = c
s.startOrder = append(s.startOrder, name)
}
func (s *Server) Run(ctx context.Context) error {
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer stop()
errCh := make(chan error, len(s.components))
for _, name := range s.startOrder {
c := s.components[name]
go func(c component) {
logrus.WithField("component", c.name).Info("Starting")
if err := c.start(ctx); err != nil {
errCh <- err
}
}(c)
}
select {
case <-ctx.Done():
logrus.Info("Shutdown signal received")
case err := <-errCh:
logrus.WithError(err).Error("Component failed, initiating shutdown")
}
s.executeShutdown()
return nil
}
func (s *Server) executeShutdown() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stopped := map[string]bool{}
for _, step := range s.shutdownSteps {
if step.fn != nil {
logrus.Info("Running shutdown func")
if err := step.fn(shutdownCtx); err != nil {
logrus.WithError(err).Error("Shutdown func failed")
}
continue
}
var wg sync.WaitGroup
for _, name := range step.componentNames {
c, ok := s.components[name]
if !ok {
logrus.WithField("component", name).Warn("Unknown component in OnShutdown")
continue
}
stopped[name] = true
wg.Add(1)
go func(c component) {
defer wg.Done()
logrus.WithField("component", c.name).Info("Stopping")
if err := c.stop(shutdownCtx); err != nil {
logrus.WithError(err).WithField("component", c.name).Error("Stop failed")
}
}(c)
}
wg.Wait()
}
// Safety net: stop any components not mentioned in OnShutdown
var wg sync.WaitGroup
for name, c := range s.components {
if stopped[name] {
continue
}
wg.Add(1)
go func(c component) {
defer wg.Done()
logrus.WithField("component", c.name).Warn("Stopping (not in OnShutdown — add it)")
if err := c.stop(shutdownCtx); err != nil {
logrus.WithError(err).WithField("component", c.name).Error("Stop failed")
}
}(c)
}
wg.Wait()
}
File 2: internal/common/server/shutdown.go
package server
import "context"
// ShutdownStep is one step in the shutdown sequence.
type ShutdownStep struct {
componentNames []string
fn func(ctx context.Context) error
}
// Stop creates a shutdown step that stops named components.
// Multiple names in one call = parallel shutdown within the step.
func Stop(names ...string) ShutdownStep {
return ShutdownStep{componentNames: names}
}
// StopFunc creates a shutdown step that runs an arbitrary cleanup function.
func StopFunc(fn func()) ShutdownStep {
return ShutdownStep{
fn: func(ctx context.Context) error {
fn()
return nil
},
}
}
// StopFuncWithErr creates a shutdown step with error return.
func StopFuncWithErr(fn func(ctx context.Context) error) ShutdownStep {
return ShutdownStep{fn: fn}
}
// OnShutdown declares the shutdown sequence.
// Steps execute top-to-bottom. Each step completes before the next starts.
// Components not mentioned are stopped last with a warning.
func OnShutdown(steps ...ShutdownStep) Option {
return func(s *Server) {
s.shutdownSteps = steps
}
}
File 3: internal/common/server/http.go (replace existing)
package server
import (
"context"
"net/http"
"os"
"{{module_common}}/auth"
"{{module_common}}/logs"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
"github.com/sirupsen/logrus"
)
func WithHTTPHandler(name string, createHandler func(chi.Router) http.Handler) Option {
return func(s *Server) {
addr := ":" + os.Getenv("PORT")
srv := &http.Server{Addr: addr}
s.addComponent(name, component{
name: name,
start: func(ctx context.Context) error {
apiRouter := chi.NewRouter()
setMiddlewares(apiRouter)
rootRouter := chi.NewRouter()
rootRouter.Mount("/api", createHandler(apiRouter))
srv.Handler = rootRouter
logrus.WithField("addr", addr).Info("Starting HTTP server")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
return err
}
return nil
},
stop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
}
}
// setMiddlewares, addAuthMiddleware, addCorsMiddleware — same as existing
File 4: internal/common/server/grpc.go (replace existing)
package server
import (
"context"
"net"
"os"
"{{module_common}}/logs"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
func WithGRPCServer(name string, registerServer func(*grpc.Server)) Option {
return func(s *Server) {
logrusEntry := logrus.NewEntry(logrus.StandardLogger())
grpcSrv := grpc.NewServer(
grpc_middleware.WithUnaryServerChain(
grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_logrus.UnaryServerInterceptor(logrusEntry),
),
grpc_middleware.WithStreamServerChain(
grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry),
),
)
registerServer(grpcSrv)
port := os.Getenv("GRPC_PORT")
if port == "" {
port = "8080"
}
addr := ":" + port
s.addComponent(name, component{
name: name,
start: func(ctx context.Context) error {
lis, err := net.Listen("tcp", addr)
if err != nil {
return err
}
logrus.WithField("addr", addr).Info("Starting gRPC server")
return grpcSrv.Serve(lis)
},
stop: func(ctx context.Context) error {
grpcSrv.GracefulStop()
return nil
},
})
}
}
Post-Creation Instructions
After creating the unified server:
- Remove or replace the old
RunHTTPServer/RunGRPCServerstandalone functions - Update all
main.gofiles to useserver.New(...).Run(ctx)withOnShutdown - Add
/threedots scaffold watermill_routerto add Watermill support - Every component MUST appear in
OnShutdown— the safety net logs warnings for forgotten ones