Stage 1: backend foundation (Postgres, sessions, accounts, OTel)
- internal/postgres: pgx-over-database/sql pool (otelsql), embedded goose
migrations into schema 'backend', committed go-jet code + cmd/jetgen tool.
- internal/account: durable accounts + unified telegram/email identities
(UUIDv7 keys), find-or-create provisioning with unique-conflict handling.
- internal/session: opaque 256-bit tokens stored as a SHA-256 hash, revoke-only
(no TTL); write-through cache gating /readyz; store + service.
- internal/telemetry: OTel tracer/meter providers (none/stdout) + request-timing
middleware; internal/config gains Postgres + OTel env loading.
- internal/server: /api/v1 {public,user,internal,admin} skeleton + X-User-ID
middleware; /readyz checks DB ping + cache; main wires
telemetry -> db+migrate -> warm cache -> server.
- Tests: unit + integration (build tag 'integration', testcontainers
postgres:17) for migrations, accounts, sessions, readyz; new integration.yaml.
- Docs: ARCHITECTURE, TESTING, PLAN refinements, root + backend READMEs.
Session/account REST handlers deferred to Stage 6 (gateway); OTLP + dashboards
to Stage 11.
This commit is contained in:
@@ -1,20 +1,30 @@
|
||||
// Command backend is the Scrabble platform's internal domain service. At this
|
||||
// stage it boots the HTTP listener with the infrastructure probes only; the
|
||||
// domain modules described in PLAN.md are added by later stages.
|
||||
// Command backend is the Scrabble platform's internal domain service. It boots
|
||||
// the OpenTelemetry runtime, opens the Postgres pool and applies migrations,
|
||||
// warms the session cache, and serves the HTTP listener with the infrastructure
|
||||
// probes and the /api/v1 route-group skeleton. Domain endpoints are added by
|
||||
// later stages described in PLAN.md.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"scrabble/backend/internal/config"
|
||||
"scrabble/backend/internal/postgres"
|
||||
"scrabble/backend/internal/server"
|
||||
"scrabble/backend/internal/session"
|
||||
"scrabble/backend/internal/telemetry"
|
||||
)
|
||||
|
||||
// telemetryShutdownTimeout bounds the OpenTelemetry flush during process exit.
|
||||
const telemetryShutdownTimeout = 5 * time.Second
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
@@ -30,12 +40,55 @@ func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
srv := server.New(cfg.HTTPAddr, logger)
|
||||
if err := srv.Run(ctx); err != nil {
|
||||
logger.Fatal("backend: server terminated", zap.Error(err))
|
||||
if err := run(ctx, cfg, logger); err != nil {
|
||||
logger.Fatal("backend: terminated", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// run wires the process dependencies in order — telemetry, database (with
|
||||
// migrations), session cache, HTTP server — and blocks until ctx is cancelled.
|
||||
func run(ctx context.Context, cfg config.Config, logger *zap.Logger) error {
|
||||
tel, err := telemetry.New(ctx, cfg.Telemetry)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init telemetry: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), telemetryShutdownTimeout)
|
||||
defer cancel()
|
||||
if err := tel.Shutdown(shutdownCtx); err != nil {
|
||||
logger.Warn("telemetry shutdown", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
db, err := postgres.Open(ctx, cfg.Postgres,
|
||||
postgres.WithTracerProvider(tel.TracerProvider()),
|
||||
postgres.WithMeterProvider(tel.MeterProvider()),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open database: %w", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
if err := postgres.ApplyMigrations(ctx, db); err != nil {
|
||||
return fmt.Errorf("apply migrations: %w", err)
|
||||
}
|
||||
logger.Info("database migrations applied")
|
||||
|
||||
sessions := session.NewService(session.NewStore(db), session.NewCache())
|
||||
if err := sessions.Warm(ctx); err != nil {
|
||||
return fmt.Errorf("warm session cache: %w", err)
|
||||
}
|
||||
logger.Info("session cache warmed")
|
||||
|
||||
srv := server.New(cfg.HTTPAddr, server.Deps{
|
||||
Logger: logger,
|
||||
DB: db,
|
||||
PingTimeout: cfg.Postgres.OperationTimeout,
|
||||
SessionsReady: sessions.Ready,
|
||||
})
|
||||
return srv.Run(ctx)
|
||||
}
|
||||
|
||||
// newLogger builds a production JSON logger at the given level.
|
||||
func newLogger(level string) (*zap.Logger, error) {
|
||||
var lvl zap.AtomicLevel
|
||||
|
||||
Reference in New Issue
Block a user