Files
Productier/apps/backend/internal/app/app.go
T
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

149 lines
3.6 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"
"go.uber.org/zap"
"productier/apps/backend/internal/authsession"
"productier/apps/backend/internal/filestorage"
"productier/apps/backend/internal/httpapi"
"productier/apps/backend/internal/mailruntime"
"productier/apps/backend/internal/store"
)
type App struct {
server *httpapi.Server
port string
shutdownTimeout time.Duration
stopMailRuntime context.CancelFunc
}
func New(logger *zap.Logger) (*App, error) {
runtimeConfig, err := loadRuntimeConfig()
if err != nil {
return nil, err
}
var dataStore store.Store
databaseURL := os.Getenv("DATABASE_URL")
if databaseURL != "" {
persistentStore, err := store.NewPostgresStore(databaseURL, runtimeConfig.mode)
if err != nil {
return nil, err
}
dataStore = persistentStore
} else {
if err := validateStoreRuntimeMode(runtimeConfig.mode, inMemoryStoreAllowed()); err != nil {
return nil, err
}
dataStore = store.NewSeededState(runtimeConfig.mode)
}
mailService, err := mailruntime.New(dataStore, logger, runtimeConfig.mailSecret)
if err != nil {
return nil, err
}
files, err := filestorage.NewFromEnv()
if err != nil {
return nil, err
}
probeCtx, cancelProbe := context.WithTimeout(context.Background(), 3*time.Second)
defer cancelProbe()
if err := files.Probe(probeCtx); err != nil {
return nil, fmt.Errorf("file storage startup probe failed: %w", err)
}
mailRuntimeCtx, stopMailRuntime := context.WithCancel(context.Background())
mailService.Start(mailRuntimeCtx)
return &App{
server: httpapi.NewServer(
dataStore,
authsession.NewClient(runtimeConfig.authServiceURL),
mailService,
files,
runtimeConfig.mode,
runtimeConfig.corsAllowOrigins,
runtimeConfig.metricsAuthToken,
logger,
),
port: runtimeConfig.apiPort,
shutdownTimeout: runtimeConfig.shutdownTimeout,
stopMailRuntime: stopMailRuntime,
}, nil
}
func (a *App) Run() error {
return a.RunContext(context.Background())
}
func (a *App) RunContext(ctx context.Context) error {
httpServer := &http.Server{
Addr: fmt.Sprintf(":%s", a.port),
Handler: a.server.Engine(),
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
serverErr := make(chan error, 1)
go func() {
err := httpServer.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
serverErr <- err
}
close(serverErr)
}()
select {
case <-ctx.Done():
if a.stopMailRuntime != nil {
a.stopMailRuntime()
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), a.shutdownTimeout)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("shutdown api server: %w", err)
}
if err, ok := <-serverErr; ok && err != nil {
return fmt.Errorf("run api server: %w", err)
}
return nil
case err, ok := <-serverErr:
if a.stopMailRuntime != nil {
a.stopMailRuntime()
}
if !ok || err == nil {
return nil
}
return fmt.Errorf("run api server: %w", err)
}
}
func validateStoreRuntimeMode(mode string, allowInMemory bool) error {
if mode == "development" || allowInMemory {
return nil
}
return fmt.Errorf("DATABASE_URL is required when APP_ENV=%q (set ALLOW_INMEMORY_STORE=true only for temporary non-production testing)", mode)
}
func inMemoryStoreAllowed() bool {
raw := strings.TrimSpace(strings.ToLower(os.Getenv("ALLOW_INMEMORY_STORE")))
switch raw {
case "1", "true", "yes", "on":
return true
default:
return false
}
}