core/cmd/twhelp/cmd_serve.go

220 lines
5.9 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
"gitea.dwysokinski.me/twhelp/corev3/internal/adapter"
"gitea.dwysokinski.me/twhelp/corev3/internal/app"
"gitea.dwysokinski.me/twhelp/corev3/internal/chislog"
"gitea.dwysokinski.me/twhelp/corev3/internal/health"
"gitea.dwysokinski.me/twhelp/corev3/internal/port"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/urfave/cli/v2"
)
var (
apiServerPortFlag = &cli.UintFlag{
Name: "api.port",
EnvVars: []string{"API_PORT"},
Value: 9234, //nolint:gomnd
}
apiServerReadTimeoutFlag = &cli.DurationFlag{
Name: "api.readTimeout",
EnvVars: []string{"API_READ_TIMEOUT"},
Value: 5 * time.Second, //nolint:gomnd
}
apiServerReadHeaderTimeoutFlag = &cli.DurationFlag{
Name: "api.readHeaderTimeout",
EnvVars: []string{"API_READ_HEADER_TIMEOUT"},
Value: time.Second,
}
apiServerWriteTimeoutFlag = &cli.DurationFlag{
Name: "api.writeTimeout",
EnvVars: []string{"API_WRITE_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
}
apiServerIdleTimeoutFlag = &cli.DurationFlag{
Name: "api.idleTimeout",
EnvVars: []string{"API_IDLE_TIMEOUT"},
Value: 180 * time.Second, //nolint:gomnd
}
apiServerShutdownTimeoutFlag = &cli.DurationFlag{
Name: "api.shutdownTimeout",
EnvVars: []string{"API_SHUTDOWN_TIMEOUT"},
Value: 10 * time.Second, //nolint:gomnd
}
apiServerOpenAPIEnabledFlag = &cli.BoolFlag{
Name: "api.openApi.enabled",
EnvVars: []string{"API_OPENAPI_ENABLED"},
Value: true,
}
apiServerOpenAPISwaggerEnabledFlag = &cli.BoolFlag{
Name: "api.openApi.swaggerEnabled",
EnvVars: []string{"API_OPENAPI_SWAGGER_ENABLED"},
Value: true,
}
apiServerOpenAPIServersFlag = &cli.StringSliceFlag{
Name: "api.openApi.servers",
EnvVars: []string{"API_OPENAPI_SERVERS"},
}
apiServerFlags = []cli.Flag{
apiServerPortFlag,
apiServerReadTimeoutFlag,
apiServerReadHeaderTimeoutFlag,
apiServerWriteTimeoutFlag,
apiServerIdleTimeoutFlag,
apiServerOpenAPIEnabledFlag,
apiServerOpenAPISwaggerEnabledFlag,
apiServerOpenAPIServersFlag,
}
)
const (
apiBasePath = "/api"
metaBasePath = "/_meta"
)
var cmdServe = &cli.Command{
Name: "serve",
Usage: "Run the HTTP server",
Flags: concatSlices(apiServerFlags, dbFlags),
Action: func(c *cli.Context) error {
logger := loggerFromCtx(c.Context)
// deps
bunDB, err := newBunDBFromFlags(c)
if err != nil {
return err
}
defer func() {
logger.Debug("closing db connections...", slog.Int("db.openConnections", bunDB.Stats().OpenConnections))
if dbCloseErr := bunDB.Close(); dbCloseErr != nil {
logger.Warn("couldn't close db connections", slog.Any("error", dbCloseErr))
} else {
logger.Debug("db connections closed")
}
}()
// adapters
versionRepo := adapter.NewVersionBunRepository(bunDB)
serverRepo := adapter.NewServerBunRepository(bunDB)
tribeRepo := adapter.NewTribeBunRepository(bunDB)
playerRepo := adapter.NewPlayerBunRepository(bunDB)
villageRepo := adapter.NewVillageBunRepository(bunDB)
ennoblementRepo := adapter.NewEnnoblementBunRepository(bunDB)
// services
versionSvc := app.NewVersionService(versionRepo)
serverSvc := app.NewServerService(serverRepo, nil, nil)
tribeSvc := app.NewTribeService(tribeRepo, nil, nil)
playerSvc := app.NewPlayerService(playerRepo, nil, nil, nil)
villageSvc := app.NewVillageService(villageRepo, nil, nil)
ennoblementSvc := app.NewEnnoblementService(ennoblementRepo, nil, nil)
// health
h := health.New()
server, err := newHTTPServer(
httpServerConfig{
port: c.Uint(apiServerPortFlag.Name),
readTimeout: c.Duration(apiServerReadTimeoutFlag.Name),
readHeaderTimeout: c.Duration(apiServerReadHeaderTimeoutFlag.Name),
writeTimeout: c.Duration(apiServerWriteTimeoutFlag.Name),
idleTimeout: c.Duration(apiServerIdleTimeoutFlag.Name),
},
func(r chi.Router) error {
oapiCfg, oapiCfgErr := newOpenAPIConfigFromFlags(c)
if oapiCfgErr != nil {
return oapiCfgErr
}
r.Use(newAPIMiddlewares(logger)...)
r.Mount(metaBasePath, port.NewMetaHTTPHandler(h))
r.Mount(apiBasePath, port.NewAPIHTTPHandler(
versionSvc,
serverSvc,
tribeSvc,
playerSvc,
villageSvc,
ennoblementSvc,
port.WithOpenAPIConfig(oapiCfg),
))
return nil
},
)
if err != nil {
return fmt.Errorf("couldn't construct HTTP server: %w", err)
}
idleConnsClosed := make(chan struct{})
go func() {
waitForShutdownSignal(c.Context)
logger.Debug("received shutdown signal")
ctx, cancel := context.WithTimeout(c.Context, c.Duration(apiServerShutdownTimeoutFlag.Name))
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Warn("shutdown failed", slog.Any("error", err))
}
close(idleConnsClosed)
}()
logger.Info("Server is listening on the addr "+server.Addr, slog.String("addr", server.Addr))
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("ListenAndServe failed: %w", err)
}
<-idleConnsClosed
return nil
},
}
func newOpenAPIConfigFromFlags(c *cli.Context) (port.OpenAPIConfig, error) {
rawURLs := c.StringSlice(apiServerOpenAPIServersFlag.Name)
servers := make([]port.OpenAPIConfigServer, 0, len(rawURLs))
for _, raw := range rawURLs {
u, err := url.Parse(raw)
if err != nil {
return port.OpenAPIConfig{}, err
}
servers = append(servers, port.OpenAPIConfigServer{
URL: u,
})
}
return port.OpenAPIConfig{
Enabled: c.Bool(apiServerOpenAPIEnabledFlag.Name),
SwaggerEnabled: c.Bool(apiServerOpenAPISwaggerEnabledFlag.Name),
BasePath: apiBasePath,
Servers: servers,
}, nil
}
func newAPIMiddlewares(logger *slog.Logger) chi.Middlewares {
return chi.Middlewares{
chislog.Logger(logger, chislog.WithFilter(func(r *http.Request) bool {
return !strings.HasPrefix(r.URL.Path, metaBasePath)
})),
middleware.Recoverer,
}
}